一图看懂 Sendable & @Sendable—— Swift 并发世界的「通行证」到底长什么样?

218 阅读3分钟

速览思维导图(先收藏,再阅读)

数据竞争
├─ 定义:≥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)
全局单例 + NSLockactor 单例 + 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 并发世界任你行。