前言
Swift 5.5 带来 async/await 与 Actor 后,「用 Actor 包一层」几乎成了默认答案。
但在日常开发里,我们经常会遇到两种尴尬:
- 只想保护一个计数器、缓存或 token,却不得不把整段逻辑都改成异步;
- 把对象放到 @MainActor 后,发现后台线程也要用,结果到处是 await。
Apple 在 Swift 5.9 前后把 Mutex 正式搬进标准库(通过 Synchronization 模块),给“同步但不想异步”的场景提供了第三条路。
Mutex 是什么(一句话先记住)
Mutex = 互斥锁,同步、阻塞、轻量。
它只干一件事:同一时刻最多一个线程进入临界区,保证对共享状态的“读-改-写”原子化。
与 Actor 的“异步消息”不同,Mutex 的等待是阻塞线程,所以临界区必须短、快、不阻塞。
基础用法:从 0 到 1 保护一个计数器
- 引入模块(Xcode 15+/Swift 5.9 自带)
1import Synchronization 2
- 定义线程安全的 Counter
1final class Counter: Sendable { // ① Sendable 空标记即可,Mutex 本身已 Sendable 2 private let mutex = Mutex(0) // ② 初始值 0 3 4 /// 加 1,同步返回 5 func increment() { 6 mutex.withLock { value in 7 value += 1 // ③ 闭包内 value 是 inout,直接改 8 } 9 } 10 11 /// 减 1 12 func decrement() { 13 mutex.withLock { value in 14 value -= 1 15 } 16 } 17 18 /// 读值,也要拿锁 19 var count: Int { 20 mutex.withLock { value in 21 return value // ④ 只读,同样原子 22 } 23 } 24} 25
- 客户端代码——完全同步
1let counter = Counter() 2counter.increment() 3print(counter.count) // 1 4
要点回顾
withLock<T>泛型返回,既能读也能写;- 闭包里的
value是inout,修改即生效; - 锁的持有时间 = 闭包运行时间,务必短。
让属性看起来“像正常变量”——封装 getter/setter
1extension Counter { 2 var count: Int { 3 get { 4 mutex.withLock { $0 } // $0 就是 value,直接返回 5 } 6 set { 7 mutex.withLock { value in 8 value = newValue 9 } 10 } 11 } 12} 13 14// 使用方无感 15counter.count = 10 16print(counter.count) // 10 17
与 @Observable 搭档——让 SwiftUI 刷新
Mutex 只保护值,不会触发属性观察器。
若直接 @Observable final class Counter,视图不会刷新。
需要手动告诉 Observation 框架:
1@Observable 2final class Counter: Sendable { 3 private let mutex = Mutex(0) 4 5 var count: Int { 6 get { 7 access(keyPath: \.count) // ① 读标记 8 return mutex.withLock { $0 } 9 } 10 set { 11 withMutation(keyPath: \.count) { // ② 写标记 12 mutex.withLock { $0 = newValue } 13 } 14 } 15 } 16} 17
SwiftUI 端无额外成本:
1struct ContentView: View { 2 @State private var counter = Counter() 3 4 var body: some View { 5 VStack { 6 Text("\(counter.count)") 7 Button("++") { counter.increment() } 8 Button("--") { counter.decrement() } 9 } 10 } 11} 12
Actor or Mutex?一张决策表帮你 10 秒选
| 维度 | Mutex | Actor |
|---|---|---|
| 同步/异步 | 同步、阻塞 | 异步、非阻塞 |
| 适用场景 | 极短临界区(赋值、累加) | 长时间任务、IO、网络 |
| 性能 | 极轻量,纳秒级锁 | 微秒毫秒,调度开销 |
| 语法侵入 | 无 async | 强制 async/await |
| Sendable | Mutex 已 Sendable,类标即可 | Actor 引用即 Sendable |
| 调试难度 | 简单,栈清晰 | 异步堆栈难追踪 |
“只想保护一两行, Mutex 别犹豫; 流程长、要并发, Actor 顶上。”
扩展场景实战
- 高频读写缓存(图片、Token)
1final class ImageCache: Sendable { 2 private let cache = Mutex([String: Image]()) 3 4 func image(for key: String) -> Image? { 5 cache.withLock { $0[key] } 6 } 7 8 func save(_ image: Image, for key: String) { 9 cache.withLock { dict in 10 dict[key] = image 11 } 12 } 13} 14
- 统计接口 QPS
1final class Stats: Sendable { 2 private let counter = Mutex(0) 3 private let start = Date() 4 5 func record() { 6 counter.withLock { $0 += 1 } 7 } 8 9 var qps: Double { 10 counter.withLock { Double($0) / start.timeIntervalSinceNow * -1 } 11 } 12} 13
- 保护非 Sendable 的 C 句柄
1final class SQLiteHandle: @unchecked Sendable { 2 private let db: UnsafeMutableRawPointer 3 public init(db: UnsafeMutableRawPointer) { 4 self.db = db 5 } 6 7 private let lock = Mutex(()) 8 9 func execute(_ sql: String) { 10 lock.withLock { _ in 11 sqlite3_exec(db, sql, nil, nil, nil) // 临界区 12 } 13 } 14} 15
踩坑与提醒
- 长任务别用 Mutex
一旦临界区阻塞 IO,整个线程池都会被卡死,比 Actor 还惨。 - 递归加锁会死锁
Mutex 不可重入,同一线程重复拿锁直接挂起;Actor 不会。 - 锁粒度要细
大对象整颗锁会变成性能瓶颈,可拆成多颗 Mutex 或按 Key 分片。 - Swift 6 数据竞争检查
打开-strict-concurrency=complete后,凡是非 Sendable 全局变量都会报错;用 Mutex 包一层即可通过。
小结
Actor 把“线程安全”装进黑盒子,让开发者用消息思考;Mutex 把“锁”暴露给你,却换回最简洁的同步代码。
两者不是谁取代谁,而是互补:
- 短、频、快 → Mutex
- 长、流、异步 → Actor
《Swift 并发编程新选择:Mutex 保护可变状态实战解析》 是转载文章,点击查看原文。
