速览思维导图(先收藏,再阅读)
数据竞争
├─ 定义:≥2 线程 + 同一块内存 + 至少一个写
└─ 后果:顺序不确定 / 崩溃 / 幽灵 Bug
Sendable(协议)
├─ 值类型:成员全部 Sendable ⇒ 自动符合
├─ 引用类型:final + 成员全部 Sendable + 无可变状态 ⇒ 手动符合
└─ 祖传代码:@unchecked Sendable + 人工保证线程安全
@Sendable(闭包注解)
├─ 要求:捕获的**所有**变量 必须 Sendable
└─ 场景:Task / TaskGroup / 自己写的并发 API
Swift 6 新宠:sending
├─ 只关心 **一次性过户**,不强制对象本身 Sendable
└─ 与 @Sendable 互补,不是替代
为什么要有 Sendable?
| 安全区域 | 危险区域 |
|---|---|
| 单线程、主线程 | 多任务、TaskGroup、actor 之间 |
| 值类型拷贝 | 引用类型共享 |
Sendable 就是编译器给你的“跨域通行证”:只要类型符合 Sendable,编译器就默认它可以安全地跨并发边界传递,无需额外同步。
值类型:天生 Sendable?
// ✅ 自动 Sendable:所有成员都是 Sendable
struct Movie {
let title: String // String 是 Sendable
let year: Int // Int 是 Sendable
}
class FormatterCache {
var name: String = "unravel"
}
// ❌ 非 Sendable:成员包含非 Sendable 引用
struct Movie {
let cache = FormatterCache() // class 且非 Sendable
}
规则: 值类型递归成员必须全是 Sendable,否则整体就不是Sendable
可手动加 Sendable 让编译器再检查一遍:
struct Movie: Sendable { // 再确认一次
let title: String
}
引用类型:自己证明“终身安全”
final class Config: Sendable { // ① 必须 final
let apiKey: String = "123" // ② 只有 let / Sendable 成员
// ③ 无 mutable stored property
}
不符合? 编译器立刻打脸:
Stored property 'state' of Sendable-conforming class 'MyClass' is mutable
祖传代码:@unchecked Sendable —— 手动关保险箱
class FormatterCache: @unchecked Sendable { // 你说了算
private var formatters: [String: DateFormatter] = [:]
private let queue = DispatchQueue(label: "cache.queue")
func formatter(for format: String) -> DateFormatter {
queue.sync { // 手动串行化
if let f = formatters[format] { return f }
let f = DateFormatter()
f.dateFormat = format
formatters[format] = f
return f
}
}
}
使用守则
- 100 % 确定已用锁/队列保护。
- 写注释 + 单元测试(多线程压力)。
- 计划迁移到 actor,逐步还债。
@Sendable 闭包:捕获列表大搜查
func performWork(_ operation: @escaping @Sendable () async -> Void)
要求:闭包里捕获的所有变量必须 Sendable。
反例:
class FormatterCache {
var name: String = "unravel"
func formatter(for str: String) {
print(str)
}
}
func myTask1(operation: @escaping @isolated(any) @Sendable () async throws -> Void) {
}
let cache = FormatterCache()
myTask1 {
cache.formatter(for: "YYYY")
}
修复
- 把
cache改成 actor 或 Sendable; - 或改用
sending
Swift 6 新关键字:sending —— 一次性通行证
class MyClass {
var count = 0
}
func foo() async {
let obj = MyClass() // 非 Sendable
Task { // Sending value of non-Sendable type '() async -> ()' risks causing data races
obj.count += 1
}
print(obj.count)
}
// Task 定义
// public init(name: String? = nil, priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async -> Success)
原理:编译器只保证“过户”后不再使用,无需对象本身终身安全。
自定义 API:
func runLater(_ body: sending @escaping () async -> Void) {
Task { await body() }
}
实战模板:把“全局锁”改成“零锁”
| 旧代码(锁) | 新代码(actor + sending) |
|---|---|
| 全局单例 + NSLock | actor 单例 + sending 闭包 |
| 手动加锁/解锁 | 编译器保证串行 |
| 测试用例难写 | await + 压力测试即可 |
actor ImageCache {
static let shared = ImageCache()
private var images: [String: Image] = [:]
func insert(_ image: Image, for key: String) {
images[key] = image
}
}
func download() async -> Image {
Image("any")
}
// 调用方
func load() async {
let cache = ImageCache.shared // actor 已 Sendable
let img = await download()
await cache.insert(img, for: "cat")
}
常见坑 & 速查表
| 场景 | 能否通过 | 修复姿势 |
|---|---|---|
class 里有 var 存储属性 | ❌ | 改 let / 改 actor / @unchecked |
| struct 成员含非 Sendable class | ❌ | 把 class 改成 actor 或加锁后 @unchecked |
| 闭包捕获非 Sendable | ❌ | 把对象改成 Sendable 或改用 sending |
| 需要长期共享可变状态 | ✅ | actor |
| 只需要一次性异步搬运 | ✅ | sending |
一句话总结
Sendable是“终身荣誉公民”——永远线程安全。@Sendable是“闭包安检门”——捕获链必须全公民。sending是“一次性签证”——过户后别再碰。
掌握这三张通行证,Swift 6 并发世界任你行。