Swift 中 async/await 无法使用传统锁的原因及案例分析

3 阅读3分钟

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
}

} ​``` 问题分析:

  1. 网络请求期间锁一直被持有,任何其他尝试访问 fetchData 的并发 Task 都会被阻塞。
  2. 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。