Swift 6 新关键字 `sending` 深度指南——从 `@Sendable` 到 `sending` 的进化之路

背景:Swift 6 的"并发安全"红线

在 Swift 5 时代,开启 Strict Concurrency 后,以下代码会报错:

swift 复制代码
class MyClass { var count = 0 }

func foo() {
    let obj = MyClass()
    Task {            // 🚨 Capture of 'obj' with non-sendable type 'MyClass'
        obj.count += 1
    }
}

Swift5时 Task的初始化方法定义

swift 复制代码
public init(
  priority: TaskPriority? = nil,
  operation: @Sendable @escaping () async -> Success
)

原因:Taskoperation 被标注为 @Sendable,意味着闭包只能捕获 Sendable 的值。

Swift 6 变化:同样的代码,不再报错。因为 Task 的签名被悄悄改成了 sending 闭包。

Swift6之后的Task初始化定义

swift 复制代码
public init(name: String? = nil, 
        priority: TaskPriority? = nil, 
       operation: sending @escaping @isolated(any) () async -> Success)

Sendablesending:一字之差,天壤之别

比较项 Sendable sending
特性 类型(struct / class / actor) 值(实例、闭包)
作用对象 类型(struct / class / actor) 值(实例、闭包)
关键词位置 类型声明处 参数/变量前
编译器要求 "永远线程安全" "转移后不再使用"即可
典型场景 全局共享、长期存活 一次性移交、临时捕获
示例 final class Box: Sendable { ... } func f(_: sending () -> Void)

一句话记忆:Sendable 是"终身荣誉",sending 是"一次性通行证"。

sending 解决了什么痛点?

痛点回顾

@Sendable 闭包要求所有捕获变量都线程安全,导致大量临时对象被迫做成 Sendable,甚至强行加锁,过度设计。

sending 的思路

编译器不再要求对象本身线程安全,只保证:

  • 该值移交给闭包后,原作用域不再访问;
  • 从而不会出现数据竞争。

合法 vs 非法 示例

✅ 合法:一次性移交

swift 复制代码
func foo() async {
    let obj = MyClass()
    Task {                // obj 被"sending"进任务
        obj.count += 1    // 只在任务内部使用
    }
    // 下面再访问 obj 会编译错误
}

❌ 非法:移交后仍访问

swift 复制代码
func foo() async {
    let obj = MyClass()
    Task { // Sending value of non-Sendable type '() async -> ()' risks causing data races
        obj.count += 1
    }
    print(obj.count)  
}

编译器会精准指出:later accesses could race。

自定义 sending 函数------语法与注意点

swift 复制代码
class ImageCache {
    func warmUp() {
        
    }
}

/// 自定义与 Task 相同能力的"发送"函数
func runLater(_ body: sending @escaping () async -> Void) {
    Task { await body() }
}

// 使用
func demo() async {
    let cache = ImageCache()          // 非 Sendable
    runLater {
        await cache.warmUp()          // 安全:cache 不再被外部持有
    }
}

语法小结:

  • sending 写在参数类型最前面,与 @escaping 位置相同。
  • 只能用于值(不能修饰类名)。
  • 编译器会插入隐形转移检查(类似 Rust 的 move 语义)。

@Sendable 的对比实验

实验项 @Sendable 闭包 sending 闭包
捕获非 Sendable 对象 ❌ 编译失败 ✅ 允许
捕获后外部再访问 ❌ 编译失败 ❌ 编译失败
跨并发域传递 ✅ 安全 ✅ 安全(靠"一次性")
长期全局共享 ✅ 适合 ❌ 不适合

结论:@Sendable 负责"终身安全",sending 负责"临时过户"。

实战场景:什么时候该自己写 sending

  1. 异步回调框架

    你的框架提供 func deferWork(_: sending () async -> Void),让用户把任意对象塞进来,而无需强迫它们做成 Sendable

  2. 并行 Map/Reduce 工具

    把大块非线程安全数据切成临时片,通过 sending 交给子任务,主线程后续不再触碰。

  3. 与遗留代码的接缝

    旧代码里大量 NSObject 子类无法改成 Sendable,用 sending 包装一次性异步迁移,渐进式现代化。

注意事项 & 最佳实践

  1. 不要返回 sending

    目前 sending 只能用于参数,不能作为返回类型,防止"转移"语义被滥用。

  2. 不要捕获 sending 变量再逃逸

    编译器会阻止你把 sending 闭包再次存到全局变量或属性,确保"生命周期唯一"。

  3. 单元测试多线程场景

    即使编译器通过,也要写压力测试:

swift 复制代码
   (0..<1000).concurrentForEach { _ in
       await foo()   // 确保无数据竞争
   }
  1. 与 actor 搭配更香

    sending 当作"进入 actor 世界的门票":

    • 主线程生成临时对象 → sending 交给 actor → 后续所有访问都在 actor 内部,零锁代码。

一张图总结(建议保存)

复制代码
Sendable
├─ 终身线程安全
├─ 可被任意并发环境长期持有
└─ 实现代价高(锁、不可变、snapshot)

sending
├─ 一次性"过户"
├─ 编译器保证"原主人不再碰"
└─ 实现代价低,适合临时对象

写在最后

  • sending 不是 @Sendable 的替代品,而是互补品:

    前者解决"临时搬迁",后者解决"长期共处"。

  • 在 Swift 6 的并发宇宙里,数据竞争被提前到编译期。

    理解并善用 sending,可以让你少写 80 % 的锁,少改 80 % 的旧代码,同时不牺牲线程安全。

相关推荐
Mr_zheng4 小时前
iOS 26 UIKit和Swift上的更新
ios·swift
YungFan4 小时前
iOS26适配指南之UISearchController
ios·swift
东坡肘子1 天前
高通收购 Arduino:历史的轮回 | 肘子的 Swift 周报 #0106
swiftui·arduino·swift
HarderCoder1 天前
Swift 基础语法全景(二):可选型、解包与内存安全
swift
HarderCoder1 天前
Swift 基础语法全景(三):元组、错误处理与断言
swift
HarderCoder1 天前
Swift 基础语法全景(一):从变量到类型安全
swiftui·swift
怪力左手2 天前
地图下载工具
开发语言·ios·swift
YGGP2 天前
【Swift】LeetCode 15. 三数之和
swift
HarderCoder2 天前
Swift 6.2 类型安全 NotificationCenter:告别字符串撞车
swift