Swift 中 async/await 无法使用传统锁的原因及案例分析
在 Swift 并发中,await 和 async 无法直接使用传统的锁(如 NSLock、NSRecursiveLock、DispatchSemaphore 等),这是由 Swift 并发模型的核心设计决定的。
一、 为什么 async/await 中不能用传统锁?
1. 线程阻塞与 Task 调度器冲突(核心原因)
Swift 的并发模型是基于协作式调度和线程池的。系统维护一个有限数量的线程池(通常等于 CPU 核心数)来执行所有的 Task。
- 传统锁(如
NSLock)是阻塞式的。当线程获取不到锁时,会陷入内核态睡眠,让出 CPU 但占用着该线程。 - 如果在
async函数中使用了NSLock并发生阻塞,执行该Task的工作线程就会被挂起。如果大量Task因为等待锁而阻塞了有限的工作线程,会导致整个并发系统线程饥饿,甚至死锁。
2. Actor 模型的设计哲学
Swift 并发推荐使用 Actor 来保护共享可变状态。Actor 本质上是一种“无锁”的同步机制,它通过将访问请求序列化到 Actor 自己的执行器上来保证互斥。在 async/await 体系下混用命令式的传统锁,破坏了 Actor 的隔离性。
3. 跨 await 边界持锁极其危险
如果在一个 async 函数中获取了锁,然后在持有锁的状态下调用了另一个 await 表达式,当前 Task 很可能会被挂起,并在 await 完成后被调度到另一个完全不同的线程上恢复执行。这违反了许多锁的实现前提,极易引发未定义行为或死锁。
二、 Bad Case 示例分析
Bad Case 1:跨 await 持有锁(引发死锁或状态混乱)
```swift class DataManager { private let lock = NSLock() private var cache: [String: Data] = [:]
// 🚨 Bad Case: 持有锁的状态下跨越了 await 边界
func fetchData(forKey key: String) async -> Data? {
lock.lock() // 1. 当前线程(假设线程A)获取锁
if let cached = cache[key] {
lock.unlock()
return cached
}
// 2. 发生网络请求,遇到 await,Task 挂起。此时锁依然被持有!
let networkData = await fetchFromNetwork(key: key)
// 3. 网络请求回来后,系统可能将 Task 分配给线程B继续执行。
// 此时在线程B上执行 unlock(),行为未定义,甚至可能崩溃。
cache[key] = networkData
lock.unlock()
return networkData
}
} ``` 问题分析:
- 网络请求期间锁一直被持有,任何其他尝试访问
fetchData的并发 Task 都会被阻塞。 await前后的线程可能发生切换,导致NSLock的所有权混乱。
Bad Case 2:在异步上下文中使用 DispatchSemaphore 造成线程池饿死
```swift class ResourcePool { private let semaphore = DispatchSemaphore(value: 1)
// 🚨 Bad Case: 使用信号量在异步上下文中做同步等待
func processResource() async {
semaphore.wait() // 1. 如果资源被占用,当前工作线程会被阻塞
await doSomeWork()
semaphore.signal()
}
}
```
问题分析:
DispatchSemaphore.wait() 是一个阻塞调用。在 Swift 并发中,阻塞工作线程是大忌。如果并发发起多个 processResource,工作线程全被卡在 wait() 上,导致系统没有额外的工作线程来推进 Task,引发死锁或严重卡顿。
三、 正确的替代方案
1. 使用 Actor 保护状态
Actor 会自动保证其内部状态的互斥访问,且不会阻塞线程。
```swift // ✅ Good Case: 使用 Actor actor DataManager { private var cache: [String: Data] = [:]
func fetchData(forKey key: String) async -> Data? {
if let cached = cache[key] {
return cached
}
let networkData = await fetchFromNetwork(key: key)
cache[key] = networkData
return networkData
}
} ```
2. 如果确实需要底层同步互斥(不涉及 await)
推荐使用 Swift 5.5 引入的 OSAllocatedUnfairLock,但绝对不跨越 await。
```swift import os
class Counter { // ✅ Good Case: 使用现代 Swift 同步原语,仅限同步上下文 private let lock = OSAllocatedUnfairLock() private var count = 0
func increment() {
lock.withLock {
count += 1
}
}
} ```
总结
在 Swift async/await 体系中,核心原则是**“不要阻塞工作线程”**。所有基于内核阻塞的传统锁都不应跨 await 使用。保护共享状态首选 Actor;保护轻量级同步状态选用 OSAllocatedUnfairLock 且绝不越界 await。