Swift 初阶 —— Sendable 协议 data races
一、data races
1、什么是 data races
官方文档对 data races 定义的解释是:

意思就是说 data races 就是多线程间没有同步地访问可变变量. 换句话说, data races 的定义就是: 在同时有读线程和写线程的情况下, 多线程间没有以串行的方式去访问同一块连续内存.
2、data races 是如何产生的
关于 data races 的产生原因, Swift 的官方文档是这样解释的:

总体意思就是说: 如果已经有一个线程在写这块内存, 但此时又有另一个线程访问这块内存内存, 那么就会产生 data races.
3、data races 例子
import Foundationfinal class Counter: @unchecked Sendable { // 为了消 warning 才用的 @unchecked Sendable, 但其实 value 是线程不安全的, 会造成 data racesvar value = 0
}let counter = Counter()
let queue = DispatchQueue.global()for _ in 0..<10_000 {queue.async {// 这里没有任何同步保护,多线程同时读写 value// 很容易出现 data race,导致最终值小于 10_000counter.value += 1}
}// 简单等待一下所有任务(非常粗糙,只是为了示例)
Thread.sleep(forTimeInterval: 1.0)
print("最终结果:\(counter.value)")

结果小于 10000 的原因是这样的, 假如只有两个线程, 且 counter.value 的值为 9997:
| 步骤 | 线程 A | 线程 B |
| 1 | 读 counter.value, 寄存器a 值为 9997 | |
| 2 | 读 counter.value, 寄存器b 值为 9997 | |
| 3 | 寄存器a 执行 +1 操作, 此时寄存器a 值为 9998 | |
| 4 | 寄存器b 执行 +1 操作, 此时寄存器a 值为 9998 | |
| 5 | 把寄存器a 的值写进 counter.value, 此时 conuter.value 为 9998 | |
| 6 | 把寄存器b 的值写进 counter.value, 此时 conuter.value 为 9998 |
于是这就导致了 counter.value 本应为 9999 的, 经过两次 +1 后, 变成了 9998.
二、 concurrency domain
1、什么是 concurrency domain
根据 Swift 的官方文档, 它是这样定义 concurrency domain 的:

中文翻译就是: 在一个 task 或 actor 的内部, 包含可变参数或可变属性的那部分代码就是一个 concurrency domain.
举个 🌰:
@MainActor
final class ViewModel: ObservableObject {@Published var message: String = "准备中…"func loadData() {// 当前方法在 MainActor 隔离域内执行Task {// 切换到后台隔离域执行耗时工作let text = await fetchRemoteText()// 回到 MainActor 更新 UIself.message = text}}private func fetchRemoteText() async -> String {// detached task 将作业放到独立的 concurrency domainawait Task.detached(priority: .background) {try? await Task.sleep(nanoseconds: 1_000_000_000) // 模拟网络延迟return "数据加载完成"}.value}
}
因为 ViewModel 是一个 Actor, 且 ViewModel 内有个可变的变量 (message) , 所以 ViewModel 就是一个 concurrency domain.
2、什么是 concurrency domain 间的传递
简单来说就是通过调 actor 函数或闭包的方式把某个可变变量或属性传进一个 actor 中执行, 又或是把某个变量或属性通过闭包捕获被某个 Task 或 actor 的闭包访问, 那么就属于跨 concurrency domain 传递.
举个🌰:
// e.g. 普通 Task 捕获值(MainActor 域 → Task 域)
struct Score: Sendable {var value: Int
}func startWork() {let score = Score(value: 42) // 创建于 MainActor(主线程)Task.detached { [score] in// 这里进入 Task 的并发域,score 被安全传递进来print("当前分数:\(score.value)")}
}// e.g. 在不同 actor 间传递
actor DataStore {private var items: [Int] = []func add(value: Int) {items.append(value)}func takeSnapshot() -> [Int] { // [Int] 默认符合 Sendableitems}
}struct Report: Sendable {let values: [Int]
}func fetchReport(store: DataStore) async -> Report {let snapshot = await store.takeSnapshot()// snapshot 从 DataStore 的 actor 域,传到调用者所在的并发域return Report(values: snapshot)
}
三、Sendable 协议
1、Sendable 协议用处
sendable 就是为了确保这个类的数据是线程安全的. 也就是说, 如果一个类遵循 sendable, 那么这个类只能被多个 task 以串行的方式访问.
2、如何遵循 Sendable 协议
2.1. 隐式遵循
符合以下情况的 Swift 会自动 (隐式) 认为它遵循 sendable:
- 所有 Actor 自动遵循 sendable
- 所有基本值类型 (Array, Int, Dict……) 都遵循 sendable
- 所有存储属性 (包括闭包) 都遵循 sendable 的 Struct
- 所有关联值类型都遵循 sendable 的 Enum
- class.
- 该 class 不是某个类 (除 NSObject) 的子类, 或 class 只继承了 NSObject
- 该 class 的所有存储属性 (包括闭包) 都遵循 sendable
- 该 class 为 final class
2.2. @unchecked Sendable & Mutex
2.2.1. @unchecked Sendable
因为 Sendable 的目的是确保同一块内存被多个 Task 串行访问, 而不是被并发访问; 因此我们可以在一个类里用 gcd 或锁实现内存的串行访问, 来达到 Sendable 的目的. 但 DispatchQueue 和 NSLock 都是引用类型, 且不遵循 Sendable, 因此 Swift 会认为这个类肯定不遵循 Sendable. 那在跨 concurrency domain 的时候如何解决呢? 其中一种方式就是用 @unchecked Sendable 告诉 Swift 这个类已经遵循 Sendable 了, 绕过 Swift 对这个类 Sendable 的检查.
举个🌰:
final class ThreadSafeCache: @unchecked Sendable {private var cache: [String: Sendable] = [:]// 虽然有可变状态,但通过队列保证了线程安全private let queue = DispatchQueue(label: "cache", attributes: .concurrent)func get(_ key: String) -> Sendable? {queue.sync {cache[key]}}func set(_ key: String, value: Sendable) {queue.async(flags: .barrier) {self.cache[key] = value}}
}
但为了防止 @unchecked Sendable 被人滥用, Swift6 提出了 Synchronization 模块的 Mutex 类型. 这个类型可以实现真正的 Sendable, 无需再依赖 @uncheck.
2.2.2. Synchronization 模块的 Mutex
Synchronization 模块的 Mutex 类型可以实现真正的 Sendable, 无需再依赖 @uncheck.
import Synchronizationfinal class ThreadSafeCache: Sendable {private let cache = Mutex<[String: Sendable]>([:])func get(_ key: String) -> Sendable? {cache.withLock {$0[key]}}func set(_ key: String, value: Sendable) {cache.withLock {$0[key] = value}}
}
四、@sendable 声明闭包线程安全
1、@sendable 用处


@sendable 就是用来修饰闭包的, 来确保闭包捕获的变量都遵循 sendable 协议; 以此来确保被闭包捕获的变量都是线程安全的.
2、如何使用 @sendable
func runLater(_ function: @escaping @Sendable () -> Void) -> Void {DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: function)
}
五、语法糖 —— 防止 Swift 的隐式推理是否遵循协议
你可以通过下面两种方式来阻止 Swift 隐式推理是否遵循协议
// ------------------------ 方式 1: @available(*, unavailable) 修饰 ---------------------------
@available(*, unavailable)
extension FileDescriptor: Sendable { }// ------------------------ 方式 2: 协议名前加 ‘~’ ---------------------------
struct FileDescriptor: ~Sendable {let rawValue: Int
}
本文参考:
Swift sendable types
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Sendable-TypesiOS data races
https://developer.apple.com/documentation/xcode/data-races
Swift6 sendable types
https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/dataracesafety#Sendable-Types
hacking with swift
https://www.hackingwithswift.com/swift/5.5/sendablehttps://www.avanderlee.com
https://www.avanderlee.com/swift/sendable-protocol-closures/
By 东坡肘子:
https://fatbobman.com/zh/posts/sendable-sending-nonsending/#unchecked-sendable
