一、问题本质
在并发场景下,State 的核心问题是:
多个任务可能同时读写同一个 State,导致竞态(race)和不一致(inconsistency)。
例如 SwiftUI / TCA 中:
Task A: 用户点击加一
Task B: 网络返回初始值
Task C: 定时刷新
如果没有约束:
- 最终状态可能不是任何一个动作的正确结果
- UI 会显示“半更新”或“回退”
- Bug 随机出现,很难复现
二、可选方案对比
| 方案 | 思路 | 优点 | 缺点 | 工程适用场景 |
|---|---|---|---|---|
| 锁(Lock / DispatchQueue.sync) | 用互斥保证一次只修改一个 State | 简单、直观 | 容易死锁 / 阻塞 UI / 难扩展 | Legacy / 小型共享资源 |
| Actor(Swift Concurrency) | 将 State 封装在 Actor 内,保证单线程访问 | 自动序列化访问、线程安全、易组合 | Actor 内逻辑仍要小心不可阻塞 | 推荐现代 Swift 并发架构 |
| 不可变数据 + CAS(copy-on-write) | 每次操作生成新 State,保证原始状态不变 | 并发安全、支持时间旅行、快照测试 | 内存占用大,复杂计算需优化 | Redux / TCA / 函数式风格 |
| Reducer(单向数据流 + Action → State) | 所有修改通过 Reducer 串行化 | 易测试、可回放、可组合、天然线程安全(配合 Actor / Store) | 需要严格遵循 Action → State 流程 | TCA / Redux / 大型复杂系统首选 |
三、工程分析
1️⃣ 锁
var state: Int = 0
let lock = NSLock()
func increment() {
lock.lock()
state += 1
lock.unlock()
}
- ✅ 简单修改安全
- ❌ 分布式 Action / 异步任务会锁住 UI,容易死锁
- ❌ 不可组合:多个锁容易互相阻塞
2️⃣ Actor(Swift 5.5+)
actor Counter {
var value = 0
func increment() { value += 1 }
}
- ✅ 并发安全
- ✅ 异步调用自动排队
- ✅ 和 async/await 天然结合
- ⚠️ 需要注意Actor 内不要调用阻塞操作
Actor 是现代 Swift 推荐方式,尤其配合 Reducer/Store。
3️⃣ 不可变数据 + Copy-on-Write
struct State {
var items: [Item]
}
let newState = oldState.withUpdatedItem(at: 3, newItem: x)
- ✅ 安全、可回放、时间旅行
- ✅ 避免竞态
- ❌ 内存占用大
- ❌ 对大数组 / 深度对象需要 memoization 或优化
4️⃣ Reducer + 单向数据流
store.send(.add(item))
- ✅ 所有状态变更经过唯一入口
- ✅ 可组合、易测试
- ✅ 可结合 Actor/Queue 实现并发安全
- ✅ 派生 State 可从唯一 State 计算,保持一致性
核心思想:谁能改 State,只有 Reducer。Action 是唯一入口。
四、现代 Swift 工程倾向
Reducer + Actor + 不可变 State = 最优组合
- Actor 或 DispatchQueue 串行化 State 修改
- Reducer 作为唯一更新点,保证单向数据流
- State 尽量不可变 / 派生 State 即时计算
- UI 只观察 State,不直接改
- 异步副作用通过 Effect / async Action 触发
TCA 就是这个思路的最佳实践:
Task {
await store.send(.networkResponse(data))
}
- store 内部 Actor 保证线程安全
- Reducer 保证更新一致
- 派生 State 计算自动更新 UI
五、总结一句话
State 一致性不是靠锁,而是靠“唯一修改入口 + 串行化 + 不可变/派生计算”保证。
在现代 Swift 并发架构里,Actor + Reducer + 不可变 State 是最稳、最可测试、最可扩展的组合。