6-16.【架构设计】在并发场景下,State的一致性如何保证?你更倾向于:锁、Actor、不可变数据,还是 reducer?

16 阅读5分钟

一、问题本质

在并发场景下,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

SolutionCore ConceptProsConsBest 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

  1. Actor/Serial Queue Serialization: Ensure atomicity of State mutation at the Store level.
  2. Unique Reducer: Acts as the sole source of business logic for state updates.
  3. Immutable State: Use Structs to ensure consistency; derive secondary states on-the-fly.
  4. 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.