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 % 的旧代码,同时不牺牲线程安全。

相关推荐
HarderCoder1 天前
Swift 中的不透明类型与装箱协议类型:概念、区别与实践
swift
HarderCoder1 天前
Swift 泛型深度指南 ——从“交换两个值”到“通用容器”的代码复用之路
swift
东坡肘子1 天前
惊险但幸运,两次!| 肘子的 Swift 周报 #0109
人工智能·swiftui·swift
胖虎11 天前
Swift项目生成Framework流程以及与OC的区别
framework·swift·1024程序员节·swift framework
songgeb2 天前
What Auto Layout Doesn’t Allow
swift
YGGP2 天前
【Swift】LeetCode 240.搜索二维矩阵 II
swift
YGGP3 天前
【Swift】LeetCode 73. 矩阵置零
swift
非专业程序员Ping4 天前
HarfBuzz 实战:五大核心API 实例详解【附iOS/Swift实战示例】
android·ios·swift
Swift社区5 天前
LeetCode 409 - 最长回文串 | Swift 实战题解
算法·leetcode·swift
YGGP7 天前
【Swift】LeetCode 54. 螺旋矩阵
swift