actor浅析
1. Actor 的定义
- Actor 是 Swift 5.5 引入的并发模型核心类型,用于解决多线程环境下的数据竞争(Data Race)问题。
- 它通过 数据隔离(Isolation 确保对共享状态的访问是串行的,从而避免并发冲突。
- 类型特性:引用类型(引用语义),但通过编译器强制隔离保护内部状态。
swift 代码解读复制代码actor BankAccount {
private var balance: Double = 0.0
func deposit(amount: Double) {
balance += amount
}
func withdraw(amount: Double) async -> Bool {
if balance >= amount {
balance -= amount
return true
}
return false
}
}
2. Actor 的核心特性
2.1 数据隔离(Isolation)
- 规则:只有 Actor 自身的方法(或
nonisolated
方法)可以直接访问其内部状态。 - 外部访问:必须通过
await
异步调用,确保访问发生在 Actor 的串行队列中。 - 编译器强制检查:Swift 编译器会阻止外部直接访问 Actor 的属性和方法(除非标记为
nonisolated
)。
swift 代码解读复制代码let account = BankAccount()
Task {
await account.deposit(amount: 100) // 必须异步调用
}
2.2 可重入性(Reentrancy)
2.2.1. Reentrancy 的核心概念
- 定义:Reentrancy 允许 actor 在异步方法挂起(如遇到
await
)时,暂时让出对其状态的独占访问权。其他任务可以在此期间调用该 actor 的方法,避免阻塞。 - 目的:提高并发性能,避免死锁,同时保持 actor 状态的安全性。
2.2.2. Reentrancy 的工作流程
假设一个 actor 方法执行如下操作:
- 同步执行:在遇到
await
之前,代码是同步执行的,此时 actor 独占其状态。 - 异步挂起:在
await
处,当前任务挂起,actor 释放对其状态的独占。 - 其他任务介入:在挂起期间,其他任务可以调用该 actor 的方法。
- 恢复执行:当
await
的异步操作完成后,actor 重新获得状态访问权,继续执行后续代码。
swift 代码解读复制代码actor BankAccount {
var balance: Double = 0
func withdraw(_ amount: Double) async {
guard balance >= amount else {
print("余额不足,等待存款...")
await Task.sleep(1_000_000_000) // 模拟异步等待
// ⚠️ 注意:此时其他任务可能修改了 balance!
guard balance >= amount else {
print("仍然余额不足")
return
}
}
balance -= amount
print("成功取款 \(amount)")
}
func deposit(_ amount: Double) {
balance += amount
print("存入 \(amount)")
}
}
2.2.3. Reentrancy 的注意事项
潜在问题
- 状态可能在挂起后改变:在
await
挂起期间,其他任务可能修改了 actor 的状态,导致恢复后的代码逻辑假设失效。- 例如,在
withdraw
方法中,可能在await
之后余额被其他任务修改,需要重新验证条件。
- 例如,在
解决方案
- 始终重新验证状态:在
await
之后,必须重新检查所有前置条件(如余额是否足够)。 - 避免依赖挂起前的临时状态:不要在挂起后假设任何状态未变。
2.2.4. Reentrancy 的优势
- 更高的并发性:避免因长时间独占 actor 状态而阻塞其他任务。
- 防止死锁:如果 actor 方法需要调用其他 actor 的方法,Reentrancy 允许这些调用交错执行,避免相互等待。
- 更自然的异步代码:允许在 actor 方法中组合多个异步操作。
2.2.5. 如何正确使用 Reentrancy
最佳实践
- 将可变状态访问限制在同步代码块:在
await
之前完成所有状态修改。 - 避免副作用:确保
await
前后的代码不依赖未重新验证的临时状态。 - 使用不可变数据:在异步操作中传递不可变数据(如结构体或
Sendable
类型)。
错误示例与修复
swift 代码解读复制代码// ❌ 错误:假设在 await 后余额未变
func withdraw(_ amount: Double) async {
if balance >= amount {
await someAsyncOperation() // 挂起期间 balance 可能被修改
balance -= amount // 可能不安全!
}
}
// ✅ 修复:重新验证状态
func withdraw(_ amount: Double) async {
if balance >= amount {
await someAsyncOperation()
// 重新检查条件
if balance >= amount {
balance -= amount
}
}
}
2.3 可发送类型(Sendable)
2.3.1 Sendable
的定义与目的
- 作用:声明某个类型的数据可以在并发环境中安全共享,即该类型的所有权可以跨线程或跨 Actor 传递。
- 核心目标:防止数据竞争(确保共享数据的不可变性或线程安全)。
- 适用场景:
- 在
Task
、async let
或 Actor 之间传递数据。 - 将闭包标记为
@Sendable
,用于跨并发域执行。
- 在
swift 代码解读复制代码struct User: Sendable {
let id: String
let name: String
}
// 跨 Task 安全传递
Task {
let user = User(id: "123", name: "Alice")
await processUser(user) // ✅ 安全
}
2.3.2. 遵循 Sendable
的条件
只有满足以下条件的类型可以安全遵循 Sendable
:
- 值类型(结构体、枚举)
- 默认遵循:所有存储属性必须为
Sendable
类型。 - 无需显式声明:如果所有属性都是
Sendable
,编译器会自动推断。
swift 代码解读复制代码// 自动推断为 Sendable(所有属性均为 Sendable)
struct Point: Sendable {
var x: Double
var y: Double
}
// 错误示例:包含非 Sendable 属性 ❌
struct UserProfile {
var name: String
var settings: NSMutableDictionary // 非 Sendable(可变引用类型)
}
- Actor 类型
- 自动遵循:所有
actor
类型隐式遵循Sendable
,因为它们内部的状态通过隔离保护。
swift 代码解读复制代码actor BankAccount { /* ... */ }
func transfer(account: BankAccount) { // ✅ 安全
Task { await account.deposit(amount: 100) }
}
- 类(Class)
- 严格限制:只有不可变(immutable)的
final
类可以显式声明为Sendable
。- 所有存储属性必须为常量(
let
)且类型为Sendable
。 - 类本身必须标记为
final
,禁止继承。
- 所有存储属性必须为常量(
swift 代码解读复制代码// 正确示例 ✅
final class AppConfig: Sendable {
let apiURL: String
init(apiURL: String) { self.apiURL = apiURL }
}
// 错误示例 ❌
class UserManager: Sendable { // 非 final,可能被继承
var lastUpdated = Date() // 可变属性
}
2.3.3 @Sendable
闭包
- 用途:标记闭包可以在并发域之间传递。
- 要求:闭包必须:
- 不捕获非
Sendable
的变量。 - 所有捕获的变量必须是
Sendable
类型或不可变值。
- 不捕获非
swift 代码解读复制代码func runAsyncTask() {
var counter = 0 // 非 Sendable 的局部变量
Task {
// 错误:闭包捕获了可变的局部变量 ❌
await someActor.updateCount { counter += 1 }
}
}
// 正确示例 ✅
func safeTask() {
let maxRetries = 3 // 不可变值(Sendable)
Task {
await someActor.retryOperation(max: maxRetries)
}
}
2.3.4 编译器检查与手动处理
- 编译器强制检查:在跨并发域传递数据时,如果类型不满足
Sendable
,编译器会报错。 - 手动标记为
Sendable
:若确定类型是线程安全的,但编译器无法推断,可强制标记(需谨慎!)。
swift 代码解读复制代码// 强制标记(需自行确保线程安全)
final class UnsafeCounter: @unchecked Sendable {
private var count = 0
func increment() { // 手动通过锁保护
lock.lock()
defer { lock.unlock() }
count += 1
}
private let lock = NSLock()
}
2.3.5 常见问题与陷阱
- 非
Sendable
类型传递
swift 代码解读复制代码class Logger { /* 非 Sendable */ }
Task {
let logger = Logger()
await logMessage(logger) // ❌ 编译错误:Logger 非 Sendable
}
- 闭包捕获可变状态
swift 代码解读复制代码var globalCounter = 0 // 全局变量(非 Sendable)
Task {
await someActor.update { globalCounter += 1 } // ❌ 不安全
}
- 强制绕过检查的风险
swift 代码解读复制代码// 可能引发数据竞争!
let unsafeArray = NSMutableArray()
Task {
await someActor.modifyArray(unsafeArray as! Sendable) // ⚠️ 强制转换危险
}
2.3.6 实际应用场景
- 跨 Actor 传递数据:确保参数和返回值是
Sendable
。 - Task 间共享配置:例如传递只读的配置对象。
- 并行集合处理:使用
withTaskGroup
时,元素需为Sendable
。
swift 代码解读复制代码func processUsers(users: [User]) async {
await withTaskGroup(of: Void.self) { group in
for user in users { // User 需为 Sendable
group.addTask { await processUser(user) }
}
}
}
2.3.7. Sendable 总结
- 关键规则:
- 值类型(结构体、枚举)通常自动遵循
Sendable
。 - Actor 类型隐式遵循
Sendable
。 - 类必须严格满足不可变条件才能标记为
Sendable
。 - 闭包需用
@Sendable
并避免捕获可变状态。
- 值类型(结构体、枚举)通常自动遵循
- 最佳实践:
- 优先使用值类型和 Actor。
- 避免在并发代码中使用可变引用类型。
- 谨慎使用
@unchecked Sendable
,确保手动实现线程安全。
3. Actor 的类型
3.1. 普通 Actor(Normal Actor)
3.1.1 定义与核心特性
- 定义:通过
actor
关键字声明的自定义类型,用于隔离和保护其内部状态。 - 作用范围:每个普通 Actor 是一个独立的实例,管理自己的串行队列。
- 数据隔离:只有 Actor 内部的方法可以直接访问其属性(或标记为
nonisolated
的方法)。 - 跨线程安全:外部访问必须通过
await
异步调用,确保线程安全。
swift 代码解读复制代码// 定义一个普通 Actor
actor Counter {
private var count = 0
func increment() {
count += 1
}
func currentValue() -> Int {
return count
}
}
// 使用示例
let counter = Counter()
Task {
await counter.increment()
let value = await counter.currentValue()
print(value) // 输出 1
}
3.1.2 使用场景
- 共享资源的串行访问:例如计数器、缓存、网络请求队列。
- 替代锁机制:比
DispatchQueue
或NSLock
更安全,编译器强制检查。
3.1.3 生命周期
- 引用类型:普通 Actor 是引用类型,通过 ARC 管理内存。
- 弱引用:可通过
weak
或unowned
避免循环引用。
swift 代码解读复制代码class ViewModel {
weak var counter: Counter? // 弱引用避免循环
}
3.2. 全局 Actor(Global Actor)
3.2.1 定义与核心特性
- 定义:全局 Actor 是一种特殊的 Actor,用于标记某些代码必须在特定的全局上下文中执行(如主线程)。
- 隐式单例:全局 Actor 通常只有一个共享实例(如
@MainActor
)。 - 强制上下文:标记为全局 Actor 的函数或属性,必须在对应的 Actor 上下文中调用。
swift 代码解读复制代码// 使用 @MainActor 确保在主线程执行
@MainActor
func updateUI() {
// 修改 UI 控件(如 UILabel、UIButton)
}
// 在非主线程调用会触发编译器错误 ❌
Task {
updateUI() // 错误:必须用 await 或标记为 @MainActor
}
// 正确调用 ✅
Task { @MainActor in
updateUI()
}
// 或者在普通 Actor 中切换上下文
actor DataLoader {
func loadData() async {
let data = await fetchData()
await MainActor.run { // 切换到主线程
updateUI(with: data)
}
}
}
3.2.2 常见全局 Actor
@MainActor
:最常用的全局 Actor,用于 UI 更新。- 自定义全局 Actor:可创建自己的全局 Actor(如
@DatabaseActor
)。
swift 代码解读复制代码// 自定义全局 Actor
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
// 标记为在 DatabaseActor 上下文中执行
@DatabaseActor
func saveToDatabase(data: Data) {
// 数据库操作
}
3.2.3 使用场景
- UI 操作:所有 UIKit/SwiftUI 的更新必须在
@MainActor
中执行。 - 资源单例:如数据库连接、文件管理器等需要全局串行访问的资源。
3.3 普通 Actor vs 全局 Actor 对比
特性 | 普通 Actor | 全局 Actor |
---|---|---|
声明方式 | 通过 actor 关键字定义类型 | 通过 @globalActor 标记协议或类型 |
实例化 | 可创建多个实例 | 通常是单例(如 @MainActor ) |
数据隔离范围 | 实例级别的隔离 | 全局级别的隔离(跨整个应用) |
典型用途 | 管理特定共享资源(如银行账户) | 主线程操作、全局单例资源访问 |
上下文切换 | 通过 await 显式切换 | 通过 @ActorName 隐式或显式切换 |
3.4 全局 Actor 的高级用法
3.4.1 将整个类型标记为全局 Actor
swift 代码解读复制代码@MainActor
class ViewController: UIViewController {
// 所有方法和属性默认在主线程执行
func updateLabel() {
label.text = "Updated" // 无需额外切换
}
}
3.4.2 选择性脱离全局 Actor
使用 nonisolated
标记不需要隔离的方法:
错误示例:
swift 代码解读复制代码@MainActor
class DataModel {
private var data: [String] = []
// 此方法在主线程执行
func addData(_ item: String) {
data.append(item)
}
// 脱离 MainActor 隔离(但需确保线程安全)
nonisolated func getDataCount() -> Int {
return data.count // 错误!无法直接访问隔离属性 ❌
}
}
正确示例:
swift 代码解读复制代码@MainActor
class DataModel {
private let data: [String] = [] // 改为不可变属性
nonisolated func safeGetCount() -> Int {
return data.count // ✅ 安全:data 是不可变的 Sendable 类型
}
}
nonisolated
的正确用法
- 访问非隔离的
Sendable
数据 若方法不依赖 Actor 的隔离状态,可以直接标记为nonisolated
:
swift 代码解读复制代码@MainActor
class UserProfile {
private var name: String // 隔离属性
let userId: String // 非隔离属性(常量 + Sendable)
nonisolated func getUserId() -> String {
return userId // ✅ 安全:访问的是不可变的 Sendable 属性
}
}
- 纯计算或协议实现
例如实现
Hashable
或Equatable
协议时,若逻辑不依赖隔离状态:
swift 代码解读复制代码@MainActor
class DataItem: Hashable {
private var id: UUID
// 必须用 nonisolated:协议方法不能是异步的
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(id) // 假设 id 是 Sendable(UUID 是值类型)
}
static func == (lhs: DataItem, rhs: DataItem) -> Bool {
lhs.id == rhs.id // 同样需要 nonisolated 标记
}
}
nonisolated
与线程安全的深层关系
- 黄金规则:
nonisolated
方法中访问的所有数据必须满足以下条件之一:- 不可变且
Sendable
(如let
常量、值类型)。 - 显式线程安全(如通过锁、原子操作)。
- 属于其他 Actor 的隔离状态(需通过
await
调用其方法访问)。
- 不可变且
3.5. actor注意事项
- 避免阻塞操作:在 Actor 内部避免同步阻塞调用(如
sleep
),否则会阻塞整个 Actor 队列。 - 死锁风险:多个 Actor 相互等待可能导致死锁。
- 合理使用
nonisolated
:标记为nonisolated
的方法无法访问隔离状态。 - 全局 Actor 的性能:频繁切换全局 Actor(如
@MainActor
)可能影响性能。
3.6. 实际应用示例
场景 1:普通 Actor 管理缓存
swift 代码解读复制代码actor ImageCache {
private var cache: [URL: UIImage] = [:]
func getImage(for url: URL) -> UIImage? {
return cache[url]
}
func setImage(_ image: UIImage, for url: URL) {
cache[url] = image
}
}
场景 2:全局 Actor 处理数据库
swift 代码解读复制代码@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
@DatabaseActor
class DatabaseManager {
func save(_ data: Data) {
// 串行化数据库写入
}
}
// 使用示例
Task {
let data = Data(...)
await DatabaseManager().save(data) // 在 DatabaseActor 上下文中执行
}
3.7 actor总结
- 普通 Actor:用于保护实例级别的共享状态,通过
actor
类型实现。 - 全局 Actor:用于全局上下文管理(如主线程),通过
@globalActor
标记。 - 核心区别:普通 Actor 是实例级别的隔离,全局 Actor 是应用级别的单例隔离。
评论记录:
回复评论: