一、问题本质
在并发场景下,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 是最稳、最可测试、最可扩展的组合。
英文版
6-16. [Architecture Design] Ensuring State Consistency in Concurrent Scenarios: Locks, Actors, Immutable Data, or Reducers?
I. The Essence of the Problem
In concurrent programming, the core issue with State is:
Multiple tasks may attempt to read or write to the same State simultaneously, leading to race conditions and inconsistency.
For example, in a SwiftUI / TCA architecture:
- Task A: User taps a "plus" button.
- Task B: A network request returns an initial value.
- Task C: A timer triggers a periodic refresh.
Without constraints, the consequences are:
- The final state might not reflect the correct outcome of any single action.
- The UI might flicker, show "half-updated" data, or unexpectedly roll back.
- "Heisenbugs" appear randomly and are extremely difficult to reproduce.
II. Comparison of Solutions
| Solution | Core Concept | Pros | Cons | Best Use Case |
|---|---|---|---|---|
| Locks (Lock / DispatchQueue.sync) | Use mutual exclusion to ensure only one mutation happens at a time. | Simple and intuitive. | Prone to deadlocks; can block the UI; hard to scale. | Legacy systems / small shared resources. |
| Actors (Swift Concurrency) | Encapsulate State within an Actor to ensure single-threaded access. | Automatic serialized access; thread-safe; easy to compose. | Logic inside Actors must be carefully written to avoid blocking. | Recommended: Modern Swift concurrent architectures. |
| Immutable Data + COW (Copy-on-Write) | Generate a new State for every operation, keeping the original intact. | Thread-safe; supports "Time Travel"; enables snapshot testing. | Higher memory footprint; complex computations require optimization. | Redux / TCA / Functional styles. |
| Reducer (UDF + Action → State) | All mutations are serialized through a central Reducer. | Highly testable; replayable; inherently thread-safe (with Actors). | Requires strict adherence to the Action → State flow. | Primary Choice: Large, complex systems. |
III. Engineering Analysis
1. Locks
Swift
var state: Int = 0
let lock = NSLock()
func increment() {
lock.lock()
state += 1
lock.unlock()
}
- ✅ Simple: Solves basic safety for simple mutations.
- ❌ Pain Points: Distributed actions or long-running async tasks can lock the UI for too long, easily leading to deadlocks.
- ❌ Non-composable: When multiple modules have their own locks, the probability of cross-blocking increases exponentially.
2. Actors (Swift 5.5+)
Swift
actor Counter {
var value = 0
func increment() { value += 1 }
}
- ✅ Native Safety: The compiler enforces thread safety.
- ✅ Auto-Queueing: Asynchronous calls are automatically serialized.
- ✅ Ecosystem Fit: Integrates naturally with
async/await. - ⚠️ Caution: Avoid calling synchronous blocking operations inside an Actor, as it will hang the execution sequence for that entire Actor.
3. Immutable Data + Copy-on-Write
Swift
struct State {
var items: [Item]
}
let newState = oldState.withUpdatedItem(at: 3, newItem: x)
- ✅ Zero Side Effects: State is always predictable; supports Undo/Redo.
- ✅ Prevents Races: Since the data is immutable, readers never encounter a "half-mutated" state.
- ❌ Overhead: For massive arrays or deeply nested objects, performance must be optimized via Memoization.
4. Reducer + Unidirectional Data Flow
Swift
store.send(.add(item))
- ✅ Single Entry Point: All state changes must pass through the Reducer, making auditing and monitoring easy.
- ✅ Convergence: Concurrent async results eventually line up at the Reducer to be merged into the State serially.
- Core Idea: Limit who can change the State and where they can do it. The Action is the only gateway to the State.
IV. Modern Swift Engineering Preferences
The Optimal Combination: Reducer + Actor + Immutable State
- Actor/Serial Queue Serialization: Ensure atomicity of State mutation at the Store level.
- Unique Reducer: Acts as the sole source of business logic for state updates.
- Immutable State: Use Structs to ensure consistency; derive secondary states on-the-fly.
- Decoupled Side Effects: Convert async results into new Actions and feed them back to the Reducer instead of mutating State inside an async callback.
Example using TCA (The Composable Architecture):
Swift
Task {
await store.send(.networkResponse(data))
}
- Store Internal Actor: Guarantees thread safety during mutation.
- Reducer: Guarantees logical consistency.
- Immutable State: Ensures the UI renders based on a stable snapshot.
V. Summary in One Sentence
State consistency is not achieved by "locking" data, but by "Single Entry for Mutation + Serialized Execution + Reactive Computation."
In modern Swift concurrent architectures, the combination of Actor + Reducer + Immutable State is the most robust, testable, and scalable approach.