Swift 引入并发模型之后,Sendable 成为跨并发域传值的重要协议。它本身并不执行同步操作,也不会在运行时检测数据竞争,而是通过编译期静态检查,明确标记出哪些类型的值可以安全地从一个任务或线程传递到另一个任务或线程。理解 Sendable 的检查规则,尤其是 @unchecked Sendable 的用途,是手动管理线程安全时无法回避的问题。开发者需要区分协议约束与运行时保护之间的差异,否则很容易在并发代码中留下难以调试的隐患。

Sendable 协议的作用与编译器检查规则
Sendable 协议没有定义任何方法或属性,它的存在完全是为了给编译器提供类型信息。当一个类型遵循 Sendable 时,编译器会检查该类型的每个存储属性是否也满足 Sendable 约束。对于结构体和枚举,如果所有成员都是 Sendable 的,编译器可以自动推导符合 Sendable;对于类,则必须满足更严格的条件:类必须是 final,并且所有存储属性都是不可变的且类型本身也是 Sendable。这样做是为了防止在并发传递过程中出现共享可变状态导致的竞态条件。
举个例子,一个只包含值类型成员的结构体可以自动获得 Sendable 能力:
struct User: Sendable {
let name: String
let age: Int
}上面的代码中,String 和 Int 都是 Sendable 的,因此 User 会在编译时自动合成 Sendable 实现。如果结构体包含一个引用类型的可变成员,编译器就会报错,因为该引用类型可能被多个并发域同时修改。例如:
final class MutableBox {
var content: String
init(content: String) {
self.content = content
}
}
struct Wrapper: Sendable {
let box: MutableBox // 错误:MutableBox 不是 Sendable
}这里的 Wrapper 无法自动遵循 Sendable,因为 MutableBox 是引用类型且具有可变状态。编译器会拒绝这个声明,要求开发者先保证 MutableBox 的线程安全性。这种静态检查机制能够帮助开发者在编译阶段发现潜在的数据竞争风险,但有时也会对已经通过其他手段保证线程安全的类型产生限制。
@unchecked Sendable 的含义与手动线程安全的边界
@unchecked Sendable 是一种编译器指令,允许开发者显式声明某个类型是 Sendable,而无需编译器进行自动检查。它向编译器和所有使用者传达一个承诺:该类型的值可以安全地在并发域之间传递,即使编译器无法通过类型成员分析来验证这一点。这个注解通常用于那些内部已经通过锁、串行队列或原子操作保证线程安全的引用类型。
使用 @unchecked Sendable 需要非常谨慎,因为它绕过了编译器最核心的安全检查。如果类型内部仍然存在未同步的可变状态,那么标记 @unchecked Sendable 只会掩盖问题,而不会解决数据竞争。例如,下面这个类就是不安全的:
final class UnsafeCounter: @unchecked Sendable {
var value = 0
func increment() {
value += 1
}
}虽然编译器允许这段代码通过编译,但多个线程同时调用 increment 方法时,value 的读写操作会发生竞态,最终结果不可预测。因此,@unchecked Sendable 并不意味着自动获得线程安全,它只是在告诉编译器:我已经手动处理了线程安全问题,请信任我。如果实际没有处理,后果由开发者自己承担。
手动管理线程安全时如何声明类型为 Sendable
当确实需要通过锁或其他同步机制来保护一个引用类型的内部状态时,可以按照以下步骤将其声明为 Sendable。首先,确保所有对共享状态的访问都经过同步保护;其次,使用 @unchecked Sendable 标记类型。下面的示例展示了一个使用 NSLock 保护计数器的类:
import Foundation
final class ThreadSafeCounter: @unchecked Sendable {
private var value = 0
private let lock = NSLock()
func increment() {
lock.lock()
value += 1
lock.unlock()
}
func getValue() -> Int {
lock.lock()
defer { lock.unlock() }
return value
}
}在这个示例中,value 的写入和读取都使用同一个 NSLock 进行保护,因此可以认为 ThreadSafeCounter 是线程安全的。标记 @unchecked Sendable 后,这个类型的实例可以被安全地传递到其他并发域中。使用锁的关键是保证所有访问路径都经过同一个锁,任何遗漏都会破坏线程安全假设。此外,还可以使用 os_unfair_lock 或串行 DispatchQueue 来替代 NSLock,但核心原则不变:先完成同步,再声明 Sendable。
另一种更推荐的方案是使用 actor 来管理隔离状态。actor 类型由编译器自动保证线程安全,并且自动符合 Sendable,不需要手动使用 @unchecked。例如:
actor CounterActor {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}如果代码中可以直接使用 actor,应该优先选择 actor,而不是手动加锁并标记 @unchecked Sendable。手动加锁容易出现死锁、锁粒度错误或遗漏保护路径等问题,而 actor 将这些复杂性交给了编译器和运行时处理。只有在无法使用 actor 的场景下,比如需要与现有同步机制或 C 库交互时,才考虑使用 @unchecked Sendable。
常见误区与最佳实践
第一个常见误区是认为 @unchecked Sendable 会为类型添加任何形式的自动同步。实际上它只是一个静态标注,运行时的行为没有任何变化。如果类型原本就存在数据竞争,加上 @unchecked Sendable 后问题依然存在。第二个误区是认为值类型就一定安全,但如果值类型内部包含引用类型成员,并且该引用类型可变,那么值类型本身也不会自动符合 Sendable。第三个误区是只对部分读写操作加锁,而另外一些访问路径忽略了同步,最终仍然会导致竞态。
最佳实践是优先使用不可变值类型。不可变值类型天然满足 Sendable,编译器可以自动推导,不需要任何额外标注。其次,如果需要共享可变状态,优先考虑 actor。actor 提供了编译器强制的隔离能力,避免了手动同步的负担。最后,如果必须使用手动同步,请封装好同步细节,将所有可变状态私有化,并保证所有公开方法都经过相同的锁或队列。标记 @unchecked Sendable 时,使用注释说明同步策略,方便后续维护者理解和检查。
此外,在代码审查中,任何 @unchecked Sendable 的出现都应该作为一个警示信号。审查者需要确认:该类型内部的每个可变状态是否都受到保护?是否存在逃逸的可变引用?锁的粒度是否合理?是否存在死锁风险?只有在确认这些问题后,@unchecked Sendable 才能被安全接受。否则,应该要求开发人员重构为 actor 或不可变类型。通过严格的使用规范,才能充分发挥 Sendable 协议在并发编程中的保护作用,同时避免 @unchecked Sendable 带来的风险。
Swift_Sendableunchecked_Sendable线程安全修改时间:2026-08-13 04:19:09