Swift Concurrency 系列第二篇的主角是 actor。语言新增了第四种类型,既不是 class,也不是 struct。
需要新增类型,说明存在严重问题。我们先从数据竞争开始准确理解。
数据竞争——最棘手的 bug 条件
数据竞争的条件很明确:两个或更多线程同时访问同一块内存,且至少有一个访问会写入。
满足条件时,结果属于未定义行为。值可能损坏、程序可能崩溃,也可能看起来什么都没发生。
具体结果取决于当天的调度运气。
final class Counter {
var value = 0
func increment() { value += 1 } // 读取-相加-写入, 3步骤
}
// 两个线程同时调用 increment()时
// 1000即使调用 value也可能不是 1000
value += 1不是原子的。在读取、相加、写入三步之间插入另一个线程,增量就会消失。
这个 bug 棘手,是因为难以复现。只有时机恰好才会触发,因此测试通过后,发布才收到偶发崩溃报告。
值类型篇提到,值类型不共享,因此数据竞争的前提消失。剩下的是以共享为目的的引用类型。
传统方案是锁、信号量和串行 DispatchQueue。
它们都能工作,但有共同弱点:完全依赖开发者自律。
只要有一处忘记加锁就前功尽弃,编译器也看不到这个错误。
这种模式很熟悉。就像可选值篇的 nil 检查、ARC 篇的循环引用,Swift 会把依赖自律的规则提升到类型系统。
现在轮到数据竞争了。
actor——保护自身状态的类型
一句话说,actor 是内置状态保护的引用类型。
actor Counter {
var value = 0
func increment() { value += 1 }
}
只需将 class 改为 actor,就能保证同一时间只有一个执行访问其存储属性。
每个 actor 都有串行执行器,方法调用会排队逐个处理。increment 的三步之间无法插入其他调用,因此不会产生竞争。
关键在于由编译器强制执行。actor 外部访问其状态或方法时,必须添加 await 才能编译。
let counter = Counter()
await counter.increment() // 外部访问 await 必需
print(await counter.value)
添加 await 的原因与第 1 篇的 suspension 概念相同。actor 忙碌时,请求必须排队,等待期间不会占用线程,而是主动让出执行权。
这不是像锁那样让线程休眠等待。函数会暂停,轮到它时再恢复。
相反,actor 内部代码无需 await 即可自由访问,因为已经处于隔离范围内。
这个“内部还是外部”的边界,就是第 1 篇介绍的隔离(isolation)。
@MainActor 是这个概念的全局版本:将“主线程”变成一个串行上下文的全局 actor,用于隔离 UI 状态。
原理与自定义 actor 相同。
可重入性——actor 最著名的陷阱
actor 看似万能,但设计上有一个陷阱:它允许可重入。
actor 方法遇到 await 时会暂停,actor 变为空闲状态。期间其他调用可以进入并执行。
原方法恢复时,actor 的状态可能已不同于暂停之前。
actor ImageCache {
var cache: [URL: Image] = [:]
func image(for url: URL) async -> Image {
if let cached = cache[url] { return cached }
let image = await download(url) // 暂停点——此期间可能有其他调用进入
cache[url] = image // 相同的 URL可能已经保存
return image
}
}
同一 URL 几乎同时收到两个请求时,两者都会看到缓存未命中并下载。数据不会损坏(actor 会阻止这一点),但逻辑会重复执行。
重要规则是:actor 能阻止底层数据竞争,但不会保证跨越 await 的逻辑一致性。
仍需在 await 前后重新确认状态假设。此例中,将进行中的下载 Task 保存到缓存即可避免重复执行。
为什么这样设计?禁止可重入就必须在 await 期间锁住 actor,两个互相等待的 actor 可能因此永久死锁。
Swift 选择无死锁系统,把逻辑一致性交给开发者。理解这种取舍,就能预测陷阱。
实际应用——actor 适合放在哪里
actor 适合做由多个异步上下文共享的可变状态所有者。
缓存、连接池、下载管理器、会话存储都是例子。凡是需要回答“谁保护这个状态?”的地方,actor 都值得考虑。
不适合的场景也很明确。第一,不共享的状态。
只在一个界面使用的 view model,使用 @MainActor 类更合适。自定义 actor 只会让 UI 访问增加不必要的 await。
第二,不可变数据。既然没有竞争,struct 或 let 就够了,坚持值类型优先。
第三,调用频率极高的热路径。跨越 actor 边界有串行化成本,每秒调用几十万次就该重新审视设计。
再补充一点:只保护一个原子计数器或标志位时,actor 可能过重。
这类场景使用 Swift 6 Synchronization 模块的 Mutex 等底层工具更轻量,也不需要 await。actor 在受保护状态与处理逻辑合为一体时最有价值。
总结
- 数据竞争由“并发访问+至少一次写入”构成未定义行为,且难以复现。锁虽然有效,却依赖自律。
- actor 是内置状态保护的引用类型。串行执行器让访问排队,编译器强制外部访问使用 await。
- @MainActor 将主线程变成全局 actor,是 UI 隔离的标准方案。
- actor 允许可重入。状态可能在 await 前后发生变化,因此除了防止底层竞争,还必须自行维护逻辑一致性。
- actor 适合做共享可变状态的所有者。对于非共享状态、不可变数据和极端热路径,它可能过重。
下一篇介绍隔离体系的最后一块:Sendable。内容包括可安全跨越隔离边界的类型,以及如何理解 Swift 6 strict concurrency 向现有代码发出的警告。

![[Swift 深入 #2] Swift actor 完整解析:如何防止数据竞争 封面图](/assets/images/posts/d7809f68-f42a-4283-a270-2c25867c9087/swift-actor-1.jpg)