用 Swift Actor 保护可变状态
Protect mutable state with Swift actors
2021年6月7日
一句话判断
Actor 不是高级可选特性——在 Swift Concurrency 的世界里,它是你保护共享可变状态的默认工具,该把锁和串行队列从你的工具箱里慢慢淘汰掉了。
这场 Session 讲了什么
Session 从数据竞争(Data Race)问题切入,展示了传统并发编程中用锁(Lock)、串行队列(Serial Dispatch Queue)保护共享状态的种种痛点:容易遗漏、难以测试、死锁风险。然后引出 Swift 5.5 的 actor 类型——一种在语言层面保证线程安全的抽象。
Actor 是一种特殊的引用类型,它的所有可变状态都被隔离(isolated)在 actor 内部。外部代码访问 actor 的属性或方法时,必须通过 await 显式标记这是一个潜在的挂起点。编译器会强制检查这条规则——如果你忘了 await,代码根本编译不过。Session 用一个银行账户的例子贯穿始终:转账操作需要同时修改两个账户的余额,用 actor 之后这些操作的串行化由编译器和运行时保证,不需要手动加锁。
值得深挖的点
Actor Isolation 的边界在哪里
Actor 的隔离不是”全有或全无”的。nonisolated 关键字可以让某些方法脱离 actor 的保护,适用于只读计算属性或纯函数。但要注意:nonisolated 方法不能访问 actor 的可变存储属性——编译器会拦住你。这个设计确保了隔离不会被意外打破。另一个容易被忽略的细节:actor 的 deinit 是 nonisolated 的,因为 actor 可能在 deinit 执行时已经没有执行者了。
Actor 与 Sendable 协议的关系
Actor 内部的状态天然受到保护,但跨 actor 传递的数据呢?这就是 Sendable 协议的作用。Swift 5.5 引入了 Sendable 用来标记”可以安全跨并发域传递”的类型。值类型(String、Int、Struct)如果只包含 Sendable 成员,自动遵循 Sendable。引用类型需要显式声明。跨 actor 传递非 Sendable 类型时,编译器会发出警告——这是数据竞争隐患的信号。
代码片段
// 一个银行账户 actor
// 所有状态修改都被 actor 隔离保护,不会出现数据竞争
actor BankAccount {
let id: String
private var balance: Double
init(id: String, initialDeposit: Double) {
self.id = id
self.balance = initialDeposit
}
// actor 内部方法,直接访问可变状态
func deposit(_ amount: Double) {
guard amount > 0 else { return }
balance += amount
}
func withdraw(_ amount: Double) -> Bool {
guard amount <= balance else { return false }
balance -= amount
return true
}
// nonisolated 不需要 await 调用,但不能访问可变状态
nonisolated var description: String {
"账户 \(id)" // 只能访问不可变属性
}
}
// 跨 actor 转账操作
// 每次访问 actor 的方法都需要 await
func transfer(amount: Double, from: BankAccount, to: BankAccount) async -> Bool {
// 先从源账户扣款
let success = await from.withdraw(amount)
guard success else { return false }
// 再存入目标账户
await to.deposit(amount)
return true
}
// 使用示例
let alice = BankAccount(id: "alice", initialDeposit: 1000)
let bob = BankAccount(id: "bob", initialDeposit: 500)
// 多个并发转账不会导致数据竞争
async let t1 = transfer(amount: 100, from: alice, to: bob)
async let t2 = transfer(amount: 200, from: bob, to: alice)
let results = await (t1, t2)
// Sendable 类型的定义
// 跨 actor 边界传递的类型必须遵循 Sendable
struct Transaction: Sendable {
let from: String // let 属性自动满足 Sendable
let to: String
let amount: Double
let timestamp: Date // Date 是值类型,自动 Sendable
}
actor TransactionLogger {
private var history: [Transaction] = []
func record(_ transaction: Transaction) {
// Transaction 是 Sendable,安全地跨 actor 传入
history.append(transaction)
}
}
最佳实践
- 新项目直接用 actor 替代
DispatchQueue(label:).sync和NSLock。老项目迁移时,先把最外层的状态管理对象改成 actor,内部细节逐步跟进。 - 不要把 actor 当成万能锁——如果一个操作涉及多个 actor 的状态协调(比如转账),应该在更高的抽象层用
async/await编排,而不是在 actor 内部嵌套 actor 调用。 - 为 actor 的关键操作编写单元测试时,注意测试方法本身需要在 async 上下文中运行(用
await调用 actor 方法),XCTest 在 Xcode 13 中已经原生支持 async 测试。
还有什么值得关注
@MainActor是一个特殊的全局 actor,标记后代码会在主线程执行——用@MainActor标记 UIViewController 的子类,就能告别DispatchQueue.main.async了。- Actor 的 reentrancy(重入)特性需要特别注意:一个 actor 方法在
await挂起期间,其他调用可以进入同一个 actor。这不是 bug,是设计,但可能导致状态不一致。 - Swift 5.5 的
@globalActor可以让你自定义全局 actor,适用于特定领域的隔离需求。