前言
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 自带)
swift
import Synchronization
- 定义线程安全的 Counter
swift
final class Counter: Sendable { // ① Sendable 空标记即可,Mutex 本身已 Sendable
private let mutex = Mutex(0) // ② 初始值 0
/// 加 1,同步返回
func increment() {
mutex.withLock { value in
value += 1 // ③ 闭包内 value 是 inout,直接改
}
}
/// 减 1
func decrement() {
mutex.withLock { value in
value -= 1
}
}
/// 读值,也要拿锁
var count: Int {
mutex.withLock { value in
return value // ④ 只读,同样原子
}
}
}
- 客户端代码------完全同步
swift
let counter = Counter()
counter.increment()
print(counter.count) // 1
要点回顾
withLock<T>泛型返回,既能读也能写;- 闭包里的
value是inout,修改即生效; - 锁的持有时间 = 闭包运行时间,务必短。
让属性看起来"像正常变量"------封装 getter/setter
swift
extension Counter {
var count: Int {
get {
mutex.withLock { $0 } // $0 就是 value,直接返回
}
set {
mutex.withLock { value in
value = newValue
}
}
}
}
// 使用方无感
counter.count = 10
print(counter.count) // 10
与 @Observable 搭档------让 SwiftUI 刷新
Mutex 只保护值,不会触发属性观察器。
若直接 @Observable final class Counter,视图不会刷新。
需要手动告诉 Observation 框架:
swift
@Observable
final class Counter: Sendable {
private let mutex = Mutex(0)
var count: Int {
get {
access(keyPath: \.count) // ① 读标记
return mutex.withLock { $0 }
}
set {
withMutation(keyPath: \.count) { // ② 写标记
mutex.withLock { $0 = newValue }
}
}
}
}
SwiftUI 端无额外成本:
swift
struct ContentView: View {
@State private var counter = Counter()
var body: some View {
VStack {
Text("\(counter.count)")
Button("++") { counter.increment() }
Button("--") { counter.decrement() }
}
}
}
Actor or Mutex?一张决策表帮你 10 秒选
| 维度 | Mutex | Actor |
|---|---|---|
| 同步/异步 | 同步、阻塞 | 异步、非阻塞 |
| 适用场景 | 极短临界区(赋值、累加) | 长时间任务、IO、网络 |
| 性能 | 极轻量,纳秒级锁 | 微秒毫秒,调度开销 |
| 语法侵入 | 无 async | 强制 async/await |
| Sendable | Mutex 已 Sendable,类标即可 | Actor 引用即 Sendable |
| 调试难度 | 简单,栈清晰 | 异步堆栈难追踪 |
"只想保护一两行, Mutex 别犹豫; 流程长、要并发, Actor 顶上。"
扩展场景实战
- 高频读写缓存(图片、Token)
swift
final class ImageCache: Sendable {
private let cache = Mutex([String: Image]())
func image(for key: String) -> Image? {
cache.withLock { $0[key] }
}
func save(_ image: Image, for key: String) {
cache.withLock { dict in
dict[key] = image
}
}
}
- 统计接口 QPS
swift
final class Stats: Sendable {
private let counter = Mutex(0)
private let start = Date()
func record() {
counter.withLock { $0 += 1 }
}
var qps: Double {
counter.withLock { Double($0) / start.timeIntervalSinceNow * -1 }
}
}
- 保护非 Sendable 的 C 句柄
swift
final class SQLiteHandle: @unchecked Sendable {
private let db: UnsafeMutableRawPointer
public init(db: UnsafeMutableRawPointer) {
self.db = db
}
private let lock = Mutex(())
func execute(_ sql: String) {
lock.withLock { _ in
sqlite3_exec(db, sql, nil, nil, nil) // 临界区
}
}
}
踩坑与提醒
-
长任务别用 Mutex
一旦临界区阻塞 IO,整个线程池都会被卡死,比 Actor 还惨。
-
递归加锁会死锁
Mutex 不可重入,同一线程重复拿锁直接挂起;Actor 不会。
-
锁粒度要细
大对象整颗锁会变成性能瓶颈,可拆成多颗 Mutex 或按 Key 分片。
-
Swift 6 数据竞争检查
打开
-strict-concurrency=complete后,凡是非 Sendable 全局变量都会报错;用 Mutex 包一层即可通过。
小结
Actor 把"线程安全"装进黑盒子,让开发者用消息思考;Mutex 把"锁"暴露给你,却换回最简洁的同步代码。
两者不是谁取代谁,而是互补:
- 短、频、快 → Mutex
- 长、流、异步 → Actor