为什么需要 Actor?
在 iOS 开发中,并发编程一直是“高并发 → 高崩溃”的重灾区。
传统锁(NSLock、os_unfair_lock、DispatchQueue.barrier)存在两大痛点:
- 容易死锁
- 无法静态检查:编译器不帮你排雷,运行时才爆炸
Swift 5.5 引入的 Actor 把“线程安全”变成了编译器义务:“只要代码能编译,就不会出现数据竞争。”
这是通过 Actor 隔离(actor isolation) 做到的。
Actor 的本质
Actor 是一个引用类型,与 class 类似,但额外拥有:
| 能力 | class | actor |
|---|---|---|
| 继承 | ✅ | ❌(final,不可继承) |
| 多线程共享可变状态 | 需手动同步 | 编译器自动同步 |
| 直接访问可变属性 | ✅ | ❌(必须 await) |
| 方法默认线程安全 | ❌ | ✅ |
一句话:Actor = class + 编译期线程安全检查器。
定义你的第一个 Actor
// 1. 用关键字 actor 而不是 class
actor Shop {
// 2. 常量可同步读取,无需 await
let id = "abc"
// 3. 可变状态,默认受 Actor 隔离保护
private(set) var itemsCount = 10
// 4. 修改状态的方法,无需加锁
func purchase() {
guard itemsCount > 0 else { return }
itemsCount -= 1
}
// 5. 只读计算属性,同样受隔离
var isSoldOut: Bool {
itemsCount == 0
}
}
与 Actor 交互的语法规则
| 访问场景 | 语法 | 是否挂起 |
|---|---|---|
| 读取常量 | shop.id | 否 |
| 读取/修改可变状态 | await shop.itemsCount | 是 |
| 调用隔离方法 | await shop.purchase() | 是 |
示例:
let shop = Shop()
// ✅ 同步读取常量
print("Shop ID:", shop.id)
// ❌ 编译错误:同步读取可变状态
// print(shop.itemsCount)
// ✅ 异步读取
Task {
await shop.purchase()
let count = await shop.itemsCount
print("剩余库存:", count)
}
Actor 隔离细则
-
外部同步访问可变状态 → 禁止
编译期直接报错:
Actor-isolated property 'itemsCount' can only be referenced asynchronously -
外部异步访问 → 必须 await
编译器插入隐式挂起点,保证同时仅一个任务进入 Actor 内部。
-
内部访问 → 可同步
Actor 自身方法之间互相调用无需 await,因为已经拿到“通行证”。
-
常量(let) → 可同步
不可变数据天生线程安全。
nonisolated:打破隔离的“逃生舱”
有时我们需要不挂起就能调用 Actor 的某些方法,例如日志、计算哈希值等只读操作:
extension Shop {
// 1. 标记为 nonisolated,脱离隔离域
nonisolated func log(_ message: String) {
// 2. 内部只能使用非隔离成员(常量或其他 nonisolated 成员)
print("[Shop \(id)] \(message)")
}
}
// 使用:无需 await
shop.log("Hello Actor")
注意:
nonisolated方法内部绝不能直接读取可变状态,否则编译器报错。- 适合纯函数、日志、Debug 描述等场景。
Sendable:跨 Actor 传输的“安检证”
Actor 保护了自己的内部状态,但数据总要“进出口”。
Swift 用 Sendable 协议标记线程安全类型,确保跨任务传递时不会引发数据竞争。
| 类型 | 是否默认 Sendable | 说明 |
|---|---|---|
| Int、String、Array、Dictionary(值类型) | ✅ | 值拷贝,线程安全 |
| struct / enum(值类型) | ✅ | 所有存储属性也必须是 Sendable |
| class | ❌ | 可变共享状态,需手动保证 |
| @MainActor 标注的类 | ✅ | 主线程独占,可安全传递 |
示例:
// ✅ struct 默认符合 Sendable(需要内部所有变量是Sendable的)
struct Owner: Sendable {
let name: String
}
actor Shop {
var owner: Owner // 编译通过
init(owner: Owner) {
self.owner = owner
}
}
// ❌ 可变 class 不符合 Sendable
// Non-final class 'MutableOwner' cannot conform to 'Sendable'; use '@unchecked Sendable'
class MutableOwner: Sendable { // 编译错误:MutableOwner is a non-final class
var name: String = ""
}
// ✅ 不可变 + final,手动标记 Sendable
final class ImmutableOwner: Sendable {
let name: String
init(name: String) { self.name = name }
}
实战建议:“能 struct 就别 class” 是并发编程里最便宜的保险。
Actor 可重入(Reentrancy):性能与风险的双刃剑
当 Actor 方法内部 await 其他异步调用时,Actor 会挂起并释放线程,此时等待队列里的其他任务可提前进入。
优点:提高吞吐量; 缺点:状态可能被并发修改,导致逻辑错误。
示例:
actor BankAccount {
var balance = 100
func withdraw(_ amount: Int) async -> Bool {
// 1. 检查余额
guard balance >= amount else { return false }
// 2. 模拟网络请求(挂起点)
await Task.sleep(nanoseconds: 100_000_000)
// 3. 再次检查时余额可能已变!
balance -= amount
return true
}
}
解决方案:
- 把“检查 + 扣款”做成原子事务,避免中间挂起。
- 使用状态机或锁变量(例如
enum State { case idle, withdrawing })显式管理重入。 - 若业务允许,直接顺序化队列(将相关操作放到同一个 Task 串行执行)。
完整实战:线程安全的购物车
import Foundation
// 1. 商品模型
struct Item: Sendable {
let id: String
let name: String
var stock: Int
}
// 2. 购物车 Actor
actor ShoppingCart {
private var items: [String: Int] = [:] // [itemID: count]
// 添加商品
func add(_ item: Item, quantity: Int = 1) -> Bool {
guard item.stock >= quantity else {
return false
}
items[item.id, default: 0] += quantity
return true
}
// 移除商品
func remove(itemID: String, quantity: Int = 1) {
if let current = items[itemID] {
let new = max(current - quantity, 0)
if new == 0 {
items.removeValue(forKey: itemID)
} else {
items[itemID] = new
}
}
}
// 总价计算(只读,可 nonisolated)
nonisolated func totalPrice(using priceProvider: (String) -> Double) -> Double {
// 注意:这里无法直接读取 items,只能把计算逻辑交给调用方
priceProvider("你自己计算")
}
// 打印清单(调试)
func dump() {
print("Cart contents:", items)
}
}
// 3. 使用
let cart = ShoppingCart()
let banana = Item(id: "b1", name: "Banana", stock: 5)
Task {
let success = await cart.add(banana, quantity: 3)
print("添加3根香蕉到购物车 \(success ? "成功" : "失败")")
await cart.dump()
let totalPrice = cart.totalPrice { str in
print("总计花费15元")
return 15
}
print("总价 \(totalPrice)")
}
RunLoop.main.run(until: Date.now + 1)
Actor 的扩展使用场景
| 场景 | 用法 | 备注 |
|---|---|---|
| 图片缓存 | actor ImageCache | 多任务同时下载,避免重复写入磁盘 |
| 数据库连接 | actor DatabaseQueue | 将 Core Data / SQLite 操作序列化 |
| 日志系统 | actor Logger | 异步写文件,不阻塞主线程 |
| 状态机 | actor StateMachine | 游戏主循环、下载管理器 |
| 限流器 | actor RateLimiter | 控制 API 调用频率 |
总结与建议
- Actor 不是银弹:它解决的是“共享可变状态”导致的数据竞争,但死锁、逻辑竞争、重入问题依然存在,需要业务层设计。
- 优先值类型:struct + let 是最便宜的线程安全。
- 减少跨 Actor 调用链:频繁
await会导致“回调地狱 2.0”,可通过批量接口或事件流聚合。 - 单元测试:使用
XCTUnwrap(await shop.itemsCount)直接断言 Actor 内部状态,无需暴露锁。 - 渐进迁移:现有项目可先把“单例管理器”改成 actor,再逐步下沉到更细粒度对象。