actor是Swift并发模型中最重要的类型之一,它通过运行时串行执行的方式保证了内部状态不会被多个线程同时访问。但一个现实问题随之而来:actor里面的数据往往需要被外部使用,比如一个管理用户会话的actor,它的会话列表需要被UI层读取。如果直接暴露,数据竞争检测就会报警;如果不暴露,actor就变成了信息孤岛。Sendable协议正是解决这个矛盾的核心机制,它回答了一个关键问题:什么样的类型可以安全地跨越隔离边界传递。

Sendable协议到底约束了什么
Sendable本身是一个标记协议,没有任何方法要求。它的含义是:一个类型为Sendable的值,可以被安全地从并发域的一个地方复制或传递到另一个地方,而不产生数据竞争。编译器会根据这个标记,在跨隔离边界传递值时插入检查。
满足Sendable有几条明确的路径。首先是不可变数据,用let声明的常量存储属性不会引发竞争,所以一个所有属性都是let且类型本身满足Sendable的结构体,会自动符合Sendable。其次是值语义,像Int、String、Array这类标准库类型已经是Sendable的,包含它们的值类型可以无条件跨域传递。最后是显式声明,对于引用类型,由于它传递的是引用而非副本,编译器无法证明其安全性,需要开发者用@unchecked Sendable自行担保内部有锁或其他同步机制保护。
struct UserProfile: Sendable {
let userID: String
let nickname: String
// 所有属性都是let且类型为Sendable,结构体自动满足要求
}
final class TokenCache: @unchecked Sendable {
private var storage: [String: String] = [:]
private let lock = NSLock()
// 内部通过锁保护可变状态,因此向编译器担保安全
func token(for key: String) -> String? {
lock.lock(); defer { lock.unlock() }
return storage[key]
}
}理解这三条路径非常重要。actor暴露数据时的所有编译报错,几乎都源于被暴露的类型不满足其中某一条。例如结构体里有一个var属性且类型是class,编译器就会拒绝它隐式满足Sendable,因为class实例是引用语义,跨域传递后两边都能修改同一块内存。
actor隔离边界的工作原理与数据暴露方式
actor的所有可变状态默认被隔离在actor的执行器上,外部无法直接读写。要从外部拿到actor内部的数据,标准做法是调用actor的异步方法。在方法内部,代码运行在actor的隔离域中,可以安全访问状态;方法返回时,返回值必须满足Sendable才能跨越边界传回调用方。这就是数据暴露的第一条通道:把内部状态转换成Sendable的值再返回。
actor SessionManager {
private var sessions: [String: Session] = [:]
func currentSessions() -> [String] {
// 返回String数组,Array与String都是Sendable,可以安全传出
return Array(sessions.keys)
}
}如果Session本身是一个满足Sendable的结构体,那么直接返回[String: Session]字典也没问题。但要注意一个细节:如果返回的类型不是Sendable,编译器会在方法签名处报错,提示return从actor隔离方法中逃逸了非Sendable值。有些开发者遇到这个报错后图省事加上@unchecked Sendable,这是危险的做法。正确思路是审视这个类型是否真的能安全传递,比如把class改成struct,或者在返回前做一次深拷贝。
第二条通道是非隔离访问。如果actor的某些状态在初始化后不再改变,可以用nonisolated把它标记为隔离区域之外:
actor ConfigProvider {
nonisolated let appVersion: String // 不可变,无需隔离保护
private var dynamicConfig: [String: String] = [:]
init(appVersion: String) {
self.appVersion = appVersion
}
}nonisolated let要求属性类型本身满足Sendable,因为初始化后它就脱离了actor的保护。这条规则经常被忽略:属性是let但类型是自定义class,仍然不能标记nonisolated。另外nonisolated也可以用在方法上,让同步方法在actor外部直接调用,但方法内部就不能访问actor的隔离状态了,只能操作传入的参数或nonisolated成员。
跨边界传递闭包与常见错误处理
除了数据本身,闭包的传递同样受Sendable约束。当你把一段闭包交给另一个并发域执行时,闭包必须是@Sendable的。被捕获的变量必须满足Sendable,否则编译器会拦截。这一点在Task和调度API中特别常见:
actor DataStore {
private var cache: [Int: String] = [:]
func fetchThenNotify() async {
let snapshot = cache // 拷贝一份Sendable数据
Task.detached {
// 这里运行在独立并发域,只能捕获Sendable值
await upload(snapshot)
}
}
}上面的代码展示了正确姿势:先在actor内部把可变状态拷贝成快照,再让快照逃逸到外部闭包。如果直接在闭包里写await self.cache反而是可以的,因为await会正确切换到actor域;但如果写cache.count不带await,编译器立刻报错,提示无法从非隔离上下文同步访问隔离属性。
处理这类报错时有几个原则值得记住。第一,看到actor-isolated报错先判断访问发生在哪个域,是同步上下文还是另一个actor或Task内部。第二,能用await异步访问就不要绕过隔离,异步等待是编译器认可的安全通道。第三,高频读取且不可变的数据,考虑用nonisolated暴露,避免每次都挂起等待。第四,确实是引用类型且有内部同步时,才使用@unchecked Sendable,并确保担保是真实的。
最后提一个容易混淆的概念:Swift 5.10之后引入的isolated参数允许方法接收一个隔离域,让多个操作在同一个actor上原子执行,这是比逐个await更精细的并发控制手段。配合Sendable的严格检查,可以构建出既灵活又没有数据竞争的接口。总体而言,Sendable与actor是一套互补的机制:actor保护可变状态,Sendable约束跨界数据,两者结合才构成了Swift并发安全的完整闭环。
Swift Sendableactor隔离并发安全修改时间:2026-09-13 08:54:29