Swift并发模型上线以来,数据竞争这个困扰多线程开发多年的问题终于有了编译期解法。Sendable协议是这套机制的核心:它告诉编译器,某个类型的实例可以安全地跨并发域传递。而在实际项目中,最常打交道的就是Actor里的字典和数组,比如一个管理连接的Actor持有[String: Client],一个消息总线Actor持有[Listener]。这些集合本身是否Sendable、集合中元素是否Sendable,直接决定了代码能否通过编译,也决定了运行期是否真正安全。本文从Sendable的语义讲起,逐步过渡到泛型类型参数上的Sendable约束,最后给出几种并发集合的实战写法。

Sendable协议到底约束了什么
Sendable是一个标记协议,类似于Combine里的空协议,它不要求实现任何方法,而是向编译器作出承诺:这个类型的实例在被多个并发域同时访问时不会引发数据竞争。编译器会根据类型结构自动判断能否作出这个承诺,判断规则大致分三类。
第一类是非泛型的struct、enum等值类型,只要所有存储属性本身都是Sendable的,编译器就自动满足Sendable,不需要显式声明。比如struct Point { var x: Int; var y: Int },Int是Sendable的,Point复制时值语义保证了隔离性,因此天然安全。
第二类是引用类型的class。class默认不满足Sendable,因为多个线程共享同一个引用时,内部可变状态随时可能被竞争。要让class满足Sendable,要么声明为final class并保证所有属性为let且类型Sendable,要么通过@unchecked Sendable绕过检查,自己承担线程安全责任,通常配合锁或队列使用。
第三类是集合类型。Swift标准库中的Array、Dictionary、Set在其Element(或Key和Value)为Sendable时才符合Sendable。这一点非常关键:[Int]是Sendable的,而[NSManagedObject]不是。换句话说,集合的Sendable性是由元素传导上来的,这正是后面泛型约束发挥作用的地方。
Actor内集合的读写安全模型
Actor用任务隔离保证内部状态的互斥访问,Actor内持有的字典、数组不需要自身是Sendable的,因为它们从不离开Actor。真正需要Sendable的是跨Actor边界传递的东西:方法的参数、返回值、以及通过continuation回传的数据。
先看一个典型的连接管理Actor,它内部的字典在Actor隔离下被安全读写,而对外暴露的返回值必须是Sendable的:
actor ConnectionManager {
// 字典本身不需要Sendable,因为被Actor隔离
private var connections: [String: ClientConnection] = [:]
func addConnection(_ conn: ClientConnection, forKey key: String) {
connections[key] = conn
}
func removeConnection(forKey key: String) {
connections.removeValue(forKey: key)
}
// 返回给外部的数据必须能安全跨域
func activeKeys() -> [String] {
Array(connections.keys)
}
}
// 假设ClientConnection是一个值类型的连接描述
struct ClientConnection: Sendable {
let id: UUID
let host: String
let port: Int
}
这里有一个容易被忽视的坑:如果把connections整个字典作为返回值暴露出去,而ClientConnection不是Sendable的,编译器会直接报错。这其实是保护——一旦字典被外部持有,Actor的隔离就形同虚设。正确的做法要么让元素类型Sendable化,要么只返回Sendable的投影数据,比如上面只返回[String]。
另一个常见场景是continuation。用withCheckedContinuation把回调式API包装成async时,resume传入的值必须Sendable,否则在严格并发检查下无法通过编译。如果需要回传一个集合,务必确认元素类型满足要求。
泛型类型参数的Sendable约束:让并发容器通用化
当你想写一个通用的并发容器,比如线程安全队列、缓存、发布订阅中心,泛型是必然选择。此时泛型参数必须加Sendable约束,否则容器无法保证安全性。对比下面两种写法:
// 写法一:没有约束,T可能是任何类型,无法安全跨域
actor WeakBox<T> {
private var value: T?
func get() -> T? { value }
}
// 写法二:T被约束为Sendable,值可以安全地进出Actor
actor SafeBox<T: Sendable> {
private var value: T?
func set(_ newValue: T) { value = newValue }
func get() -> T? { value }
}
写法一在Swift 6的严格并发检查下会报错或产生警告,因为get()把一个非Sendable的值传出Actor隔离域,等于在编译期就埋下了数据竞争的种子。写法二通过T: Sendable把安全性前置到使用处:调用方传入的类型如果不是Sendable,编译立即失败,错误信息清晰地指向问题源头。
下面是一个更完整的通用并发缓存示例,展示了字典与泛型约束的配合:
actor ConcurrentCache<Key: Hashable & Sendable, Value: Sendable> {
private var storage: [Key: Value] = [:]
func value(forKey key: Key) -> Value? {
storage[key]
}
func setValue(_ value: Value, forKey key: Key) {
storage[key] = value
}
func removeAll(where shouldBeRemoved: (Key, Value) -> Bool) {
storage = storage.filter { !shouldBeRemoved($0.key, $0.value) }
}
}
// 使用示例
let cache = ConcurrentCache<String, [Int]>()
Task {
await cache.setValue([1, 2, 3], forKey: "nums")
let result = await cache.value(forKey: "nums")
print(result ?? [])
}
注意上面的Key和Value都加了约束。Key需要Hashable才能做字典键,同时Sendable保证键可以跨Actor传入;Value的Sendable约束则保证值进出缓存都安全。这种约束组合是并发容器设计的标准范式,标准库的AsyncStream、AsyncChannel也都是这么做的。
@unchecked Sendable的使用边界
有些类型无法自然满足Sendable,比如包装了Objective-C对象、持有内部可变状态但由锁保护的容器。这时@unchecked Sendable是逃生舱,但必须明确它的含义:编译器不检查,责任全在开发者。
final class LockedCounter: @unchecked Sendable {
private let lock = NSLock()
private var count = 0
func increment() -> Int {
lock.lock()
defer { lock.unlock() }
count += 1
return count
}
}
这个例子中,count的可变性本应阻止Sendable,但NSLock保证了访问互斥,所以@unchecked Sendable是合理的。判断标准很简单:如果你能证明所有跨线程访问都有同步机制保护,就可以用;只要有一丝不确定,优先改造类型结构(改成struct、改成Actor、或者把可变部分收敛到Actor内),而不是滥用这个标注。实践中一个实用的准则是:每写一个@unchecked Sendable,就在注释里写清楚安全性依据,方便后续维护者审查。
常见错误与排查思路
实际编码中,围绕集合与Sendable的编译报错主要集中在三类。第一类是闭包捕获:在Task {}或TaskGroup中捕获了非Sendable的集合,比如捕获了某个class实例的数组。解决思路是尽量在闭包外先取出Sendable的快照,或者在Actor方法内部完成计算后再返回结果。
第二类是协议要求与Sendable的冲突:如果协议没有声明: Sendable`,任何existential类型的数组如[any Service]都不是Sendable的。修复方式是给协议加上Sendable继承,protocol Service: Sendable,这要求所有实现方也满足Sendable,通常配合actor或不可变struct实现。
第三类是泛型方法的参数逃逸:在泛型方法里把T类型的值传给另一个并发域,即使外层类型有约束,方法级别也需要相应的约束声明。排查这类问题时,看编译器报错指向的边界在哪里——Sendable的检查永远发生在跨越隔离域的那一行,顺着这条线索往往能快速定位问题结构。
总体来说,Sendable约束看似增加了编码负担,实则是把过去运行期偶现、极难复现的数据竞争,转化为编译期一行明确的错误提示。理解值语义、Actor隔离与泛型约束三者如何协作,就能在并发集合的设计中做到既通用又安全,写出经得起Swift 6严格检查考验的代码。
Swift SendableActor 并发安全泛型约束修改时间:2026-09-14 21:38:48