🚀

Swift 6 线程安全:用 Mutex 替代 @unchecked Sendable

TL;DR:在 Swift 6 中,构建线程安全的引用类型不再需要在 actor(强制 async)和 NSLock + @unchecked Sendable(失去编译器检查)之间妥协。新的 Synchronization 框架提供了完美的同步解决方案。

在并发代码中,构建一个线程安全的引用类型(class)一直是 Swift 开发者的痛点。如果不使用 actor(因为 API 会被迫变成 async),常见做法是手动加锁并使用 @unchecked Sendable 强行“安抚”编译器。然而,这只是让编译器闭嘴,一旦锁的使用出现偏差,数据竞争依然会在运行时发生。

Swift 6 引入了 Synchronization 框架,其中的 Mutex 是官方提供的、面向并发模型设计的同步原语。

为什么选择 Mutex?

Mutex 的关键价值在于编译器理解它的并发语义。它将“可变性”与“并发安全”明确绑定,使 Sendable 合规性不再依赖开发者的自觉,而是可被编译器验证。

Swift
import Synchronization

// 1. 不需要 @unchecked,编译器能自动推断其符合 Sendable
final class SafeCache: Sendable {
    
    // 2. Mutex 内部封装了可变状态,let 声明确保引用稳定
    private let storage = Mutex<[String: String]>([:])

    func update(key: String, value: String) {
        // 3. 必须在 withLock 闭包中才能访问底层数据
        // 编译器强制保证了临界区的安全性
        storage.withLock { dict in
            dict[key] = value
        }
    }

    func value(for key: String) -> String? {
        // 同步 API 得以保留,无需 async/await
        storage.withLock { dict in
            dict[key]
        }
    }
}

关键注意事项

  1. 状态容器机制Mutex 不是传统意义上的锁语句,而是一个状态容器。可变状态必须完全封装在 Mutex 泛型内部,不能将内部引用的指针泄漏到 withLock 之外。
  2. 避免复杂逻辑:和传统锁一样,withLock 的闭包应保持短小精悍,避免执行耗时操作或嵌套调用,以防死锁或性能瓶颈。
  3. Actor 的补充Mutex 并不取代 Actor。当你需要同步 API(例如在 computed property 中)、追求极低开销或需要与遗留同步代码集成时,Mutex 是比 Actor 更合适的选择。
  4. 性能优势:作为标准库的一部分,Mutex 在底层进行了高度优化,在非竞争状态下的开销极低。

延伸阅读

相关提示

订阅 Fatbobman 周报

每周精选 Swift 与 SwiftUI 开发技巧,加入众多开发者的行列。

立即订阅