Swift 的并发模型从编译器层面就开始介入数据竞争检查,这一点在函数和闭包上体现得尤其明显。闭包不像整数或字符串那样只是一个纯数据值,它内部还包含了一段可执行代码以及捕获环境。把一个闭包跨并发域传递时,真正被传递的其实是代码体加上一组对外部变量的访问权。@Sendable 的作用就是对这组访问权做一次静态审查,确保闭包在另一个执行上下文里运行时,不会通过捕获变量引发并发数据竞争。下面先围绕 Sendable 函数类型、捕获值的复制和引用限制、函数参数传递以及绕过检查的边界展开。

Sendable 对函数类型意味着什么
在 Swift 中,函数类型本身具有引用语义,闭包对象通常分配在堆上。一个普通的函数类型不能直接跨 actor 边界或并发任务边界传递,除非它被标记为 @Sendable。这个标记可以出现在闭包字面量之前,也可以出现在函数类型的参数或返回值位置。例如定义一个新的并发任务时,Task.detached 的 operation 参数就要求传入 @Sendable 闭包。下面的代码可以编译通过,因为闭包没有捕获任何外部变量,默认满足 Sendable 约束。
let work: @Sendable () -> Void = {
print("Running in concurrent context")
}
Task.detached(operation: work)
一旦闭包捕获了外部变量,Sendable 检查就不再只看函数本身,还会检查捕获列表。如果捕获的是不可变的值类型,且该类型遵循 Sendable,这个闭包就是安全的。编译器会允许它在不同线程之间传递。对于引用类型来说,检查会更严格,因为闭包捕获一个类实例时,实际上只持有了这个实例的引用,多个上下文可能同时访问同一块内存。只有这个类本身遵循 Sendable,并且其可变状态也受到保护,整个闭包才能获得通过。
函数类型在 actor 隔离方法中也有类似约束。一个 actor 的实例方法如果接受闭包参数,这个闭包很可能在 actor 的执行器之外被调用,也可能在 actor 内部被调度。若闭包不是 @Sendable,编译器会直接报错,因为它可能把非隔离的状态带入 actor 的串行执行环境中。理解这一点有助于区分闭包 Sendable 检查和普通类型 Sendable 检查的差异:前者不仅检查值本身,还要检查闭包体的可执行上下文。
捕获值的复制与引用限制
值类型捕获在 @Sendable 闭包中通常表现为复制语义。例如一个包含 String 和 Int 的 Config 结构体遵循 Sendable,闭包在创建时会复制一份 config 的值,后续原作用域中 config 的变化不会影响闭包内部的值。这种复制行为符合并发安全的要求,因为每个执行上下文都拥有独立的副本,不会共享可变存储。
struct Config: Sendable {
let host: String
let port: Int
}
let config = Config(host: "127.0.0.1", port: 8080)
let closure: @Sendable () -> String = {
return config.host
}
Task.detached {
print(closure())
}
引用类型捕获则完全不同。闭包捕获一个类实例时,默认只复制引用,不会复制对象本身。如果这个类不是 final 且没有遵循 Sendable,编译器会拒绝在 @Sendable 闭包中使用它。即便类遵循了 Sendable,如果它内部包含可变属性,仍然可能产生数据竞争。一个可靠的引用类型捕获示例如下,TokenStore 只暴露不可变属性,因此可以安全跨并发域传递。
final class TokenStore: Sendable {
let token: String
init(token: String) { self.token = token }
}
let store = TokenStore(token: "abc")
let closure: @Sendable () -> String = {
return store.token
}
捕获可变局部变量是最常见的错误场景。即使捕获的是 Int 这种值类型,闭包对 var 变量的捕获仍然使用引用语义,编译器会拒绝将这样的闭包标记为 @Sendable。下面这段代码无法通过编译,因为 counter 在闭包内部被修改,编译器无法保证并发环境下的互斥访问。
var counter = 0
let closure: @Sendable () -> Int = {
counter += 1
return counter
}
如果只是读取一个可变局部变量的值,可以使用捕获列表创建不可变副本。捕获列表会在闭包创建时对值类型做一次显式复制,这样闭包内部访问的是副本而非原变量,编译器会认为这是安全的。
var counter = 0
let closure: @Sendable () -> Int = { [counter] in
return counter
}
下表对比了值类型捕获和引用类型捕获在 @Sendable 闭包中的主要差异。
| 捕获内容 | 默认行为 | Sendable 要求 | 是否复制底层数据 |
|---|---|---|---|
| 不可变值类型 | 复制到闭包上下文 | 类型需遵循 Sendable | 是 |
| 可变值类型局部变量 | 引用原变量 | 编译器拒绝 | 否,除非捕获列表显式复制 |
| final 引用类型 | 复制引用 | 类需遵循 Sendable 且状态安全 | 否 |
| 非 Sendable 引用类型 | 复制引用 | 编译器拒绝 | 否 |
函数参数传递与 actor 隔离域
很多并发 API 不仅要求闭包是 @Sendable,还要求它作为参数被转移时保持并发安全。例如一个网络请求完成回调,如果希望回调在后台任务中执行,需要把回调参数声明为 @escaping @Sendable。这样调用方传入的闭包会从当前作用域转移出去,编译器会检查闭包捕获的所有内容是否符合 Sendable 约束。
func fetchRemoteData(completion: @escaping @Sendable (String) -> Void) {
Task.detached {
let result = "fetched data"
completion(result)
}
}
在 actor 隔离方法中,函数参数传递的约束更加明显。actor 的方法只能在自己的执行器上串行执行,但传入的闭包可能来自其他并发域。如果闭包不是 @Sendable,它可能捕获调用方的可变状态,在 actor 内部执行时就会绕过 actor 的隔离保护。因此 Swift 对 actor 方法的闭包参数也有 Sendable 要求。下面的 DataStore 示例展示了如何声明一个接受 Sendable 转换函数的 actor 方法。
actor DataStore {
private var value = 0
func applyTransform(_ transform: @Sendable (Int) -> Int) {
value = transform(value)
}
func currentValue() -> Int {
return value
}
}
调用方在传入闭包时,闭包内部不能捕获非 Sendable 的引用类型,也不能修改捕获的可变变量。这样 actor 内部的串行执行和闭包外部的并发调用之间就不会出现共享可变状态。需要特别注意的是,非逃逸函数参数一般不需要显式 @Sendable,因为它在当前作用域内同步执行,不会跨并发域转移。一旦函数参数被标记为 @escaping 且用于 Task 或 actor 隔离方法,Sendable 约束就开始生效。
@unchecked Sendable 与常见陷阱
有时开发者确实需要通过编译器检查,但类型内部使用了锁、队列或其他同步机制来保证线程安全。Swift 提供了 @unchecked Sendable,它告诉编译器跳过自动检查,由开发者自行承担数据竞争责任。这种标记不能随意使用,只有当类型确实通过内部同步机制保证了并发访问安全时,才适合添加。下面是一个使用 NSLock 保护可变状态的计数器,标记为 @unchecked Sendable 后可以被 @Sendable 闭包跨并发域捕获。
final class LockedCounter: @unchecked Sendable {
private let lock = NSLock()
private var _value = 0
var value: Int {
lock.lock()
defer { lock.unlock() }
return _value
}
func increment() {
lock.lock()
defer { lock.unlock() }
_value += 1
}
}
let counter = LockedCounter()
let closure: @Sendable () -> Int = {
counter.increment()
return counter.value
}
@unchecked Sendable 的陷阱在于它不会检查内部状态是否真的安全。例如一个类内部包含一个可变数组,但没有加锁或使用 actor 隔离,标记为 @unchecked Sendable 后仍然可以通过编译,但在多个线程访问时会产生数据竞争甚至崩溃。因此使用这个标记之前,应该先确认类型的可变状态是否只通过串行队列、锁或 actor 访问。另一个常见陷阱是在 @Sendable 闭包中捕获 self,如果 self 所在类不是 Sendable,即使闭包只读取 self 的不可变属性也无法通过编译。解决方案是让类遵循 Sendable,或者在闭包中使用捕获列表创建一个满足 Sendable 的局部副本。
还有一点需要留意,闭包的逃逸属性与 Sendable 属性是独立的。@escaping 只表示闭包会逃逸出当前作用域,而 @Sendable 才涉及并发安全。一个闭包可以只 @escaping 而不 @Sendable,在单线程异步回调中仍然可以使用;但一旦回调可能在不同线程或任务中执行,就必须同时满足两者。理解这些约束的边界,可以避免在 Swift 并发代码中写出看似正常、实际上存在数据竞争隐患的闭包捕获逻辑。
Swift Sendable@Sendable闭包并发捕获语义修改时间:2026-09-20 22:22:15