Swift Concurrency 系列 05|Actor 是什么?它和传统线程安全写法有什么区别?
Actor 不是“Swift 版锁”,它真正改变的是共享状态该如何被组织
很多人第一次听到 Actor,都会下意识把它理解成“Swift 提供的一种线程安全工具”。
这个理解不完全错,但如果只停在这里,后面很容易写出“用了 Actor,结构还是乱”的代码。
因为 Actor 最重要的地方,不是又给你多了一把新锁,而是它改变了一个默认思路:
共享可变状态不应该先暴露给所有人,再想办法保护;更合理的做法是先把它隔离起来,再决定外部如何访问。
这听起来像是措辞上的区别,实际上是并发设计习惯上的分水岭。
一、并发里最危险的东西,往往不是异步调用本身,而是共享可变状态
多数并发 bug 的起点都不是“发了一个异步请求”,而是:
- 两个任务同时读写同一份缓存
- 一个任务还没写完,另一个任务已经基于旧值做了判断
- 多个页面同时修改一份全局状态
- 某个服务既负责刷新 token,又负责消费 token,还负责把结果广播出去
这些问题看上去形式很多,但根源常常是一样的:
共享可变状态没有明确边界。
而一旦状态边界不清,任何任务调度顺序变化都可能把 bug 放大出来。
二、传统线程安全方案的问题,不是不够强,而是不够集中
过去我们常用的方案有:
NSLock- 串行队列
- 读写锁
- “这个对象只能在某个 queue 上访问”的团队约定
这些方案都不是错,今天很多成熟系统仍然在稳定使用。
它们真正的问题不是机制弱,而是:
访问规则容易散。
一开始你可能还能记得:
- 这个缓存必须加锁访问
- 那个字典只能在串行队列里改
- 某个状态只能在主线程读写
但随着调用点越来越多,这些规则就会慢慢失真。
并发 bug 最烦人的地方,往往不是你没有工具,而是你再也没法保证全团队一直严格遵守那些零散规则。
三、Actor 的核心思路不是“保护共享状态”,而是“先减少共享”
这是最值得反复强调的一点。
传统思路更像:
- 先有一份共享状态
- 再想办法通过锁、队列、约定把它保护起来
Actor 的思路则更接近:
- 这份状态本来就不应该被任意访问
- 我先把它放进隔离边界
- 外部只能通过受控接口和它交互
这带来的最大变化是:你开始被迫思考状态归属,而不是默认所有人都能直接摸到它。
例如一个 token 刷新协调器,如果只是一个全局对象加一堆锁,你的默认心智是“大家都在用这份状态,我得保护它”。
如果它是 Actor,你的默认心智会变成“这份状态归它管理,别人只能跟它要结果或发命令”。
这就是设计方式的变化。
四、为什么这种隔离式思路更适合复杂项目
因为复杂项目最怕的不是单个点写错,而是规则扩散。
一旦状态访问被 Actor 收口,很多问题会更早暴露:
- 这份数据到底归谁管
- 外部需要的是快照、派生值,还是某个命令式操作
- 哪些操作必须串行语义,哪些只是只读查询
这些问题在旧模式里常常会被拖到很后面,因为大家默认“先共享,有问题再加锁”。
而 Actor 逼你更早做边界设计。
这也是为什么我更愿意把 Actor 看成并发设计工具,而不只是线程安全工具。
五、Actor 最适合放在哪里
不是所有对象都值得变成 Actor。
它更适合那些同时满足下面几个特征的角色:
- 有共享可变状态
- 会被多个任务并发访问
- 一致性很重要
- 访问方式需要被集中协调
典型例子包括:
- 图片缓存协调器
- Token 刷新协调器
- 下载任务注册中心
- 某类全局资源访问器
- 需要串行消费事件的消息中心
这些对象的共性是:它们不是页面局部状态,而是跨任务共享、又很容易出并发问题的状态中心。
如果只是一个纯函数 service,或者一个只在单页面内部短暂使用的状态对象,未必需要 Actor。
六、Actor 和“给整个类都加个锁”到底有什么本质差别
表面看,它们都能做到“多个任务别把状态改坏”。
但工程体验差别很大。
给整个类加锁时,常见问题是:
- 调用方还是能拿到很多内部细节
- 锁的粒度容易失控
- 某些方法里又去调用别的同步资源,形成复杂嵌套
- 团队成员仍然可能绕开规则,直接访问本不该访问的状态
而 Actor 至少在语义层明确表达了:
- 这份状态不是自由共享的
- 跨隔离访问需要显式经过异步边界
- 访问这份状态本身就是一种受约束的行为
也就是说,Actor 不只是“帮你加了保护”,而是让不安全的访问方式更难被随手写出来。
七、Actor 解决不了什么
这点必须讲清楚。
很多人学到 Actor 后,会产生一种错觉:以后线程安全都交给它了。
实际上 Actor 只解决一类问题:隔离共享状态。
它解决不了:
- 业务时序本身是否合理
- 页面离开后旧任务是否该继续
- 结果是否已经过期
- 你的状态边界是不是本来就划错了
举个例子,假设你把搜索结果缓存做成了 Actor,但页面仍然允许旧请求覆盖新关键字结果,那 bug 一样还在。
因为这里的问题不是共享状态被并发写坏,而是“结果有效性”本来就没被建模。
所以 Actor 很重要,但它不是并发万灵药。
八、Actor 最容易被误用的两个方向
1. 什么都想塞进 Actor
一旦把 Actor 当成“只要用了就更安全”的标签,你就会开始过度包装:
- 明明只在单线程上下文里用的状态,也包成 Actor
- 明明更适合做纯函数处理的逻辑,也强行塞进 Actor
- 最后整个系统到处都是异步访问边界,结构反而变重
Actor 不是越多越好,关键是放在真正需要隔离共享状态的地方。
2. 把 Actor 当“安全证明”
有些代码一加上 Actor,团队就会默认这块“已经线程安全了”。
但很多 bug 根本不是来自数据竞争,而是来自:
- 错误的生命周期设计
- 错误的状态流
- 错误的任务关系
Actor 能保证“别乱并发摸我内部状态”,但不能保证“你的业务流程是对的”。
九、一个更实用的判断问题
如果你在犹豫某个对象要不要设计成 Actor,我建议先别问“它会不会线程不安全”,而是先问:
- 它内部是不是有重要的共享可变状态?
- 这个状态会不会被多个任务同时访问?
- 我是否希望外部只能通过受控接口和它交互?
- 如果不加隔离边界,团队以后是否很容易随手乱用它?
如果这些问题大部分答案都是“是”,那它就很可能适合 Actor。
十、结论:Actor 的真正价值,是把共享可变状态从默认暴露变成默认隔离
如果只用一句话总结,我会说:
Actor真正改变的,不是“你现在多了一种锁”,而是“共享可变状态终于不再默认暴露给所有人”。
传统思路更像是:先共享,再补保护。
Actor 的思路则是:先隔离,再决定如何访问。
而在复杂并发系统里,这种思路变化往往比“多一种线程安全工具”更重要。