Sendable — 是一个 Swift 协议,用于标记可在线程之间安全传输的类型。在使用并发编程(actor、async/await、Task)时,Swift 编译器要求在隔离上下文之间传输的所有数据都必须符合 Sendable。这排除了意外传输可能导致数据竞争的不安全类型。该协议作为一份合约:符合 Sendable 的类型保证不存在内部未同步状态。据 WWDC 2021,Sendable 协议是设计安全多线程架构时的必要元素。
主要内容
Sendable — 是来自 Swift 标准库(SE-0302)的一个协议,用于标记可在隔离域之间安全传输的类型。隔离域包括 actor、Task 任务或 @MainActor 上下文。Sendable 不包含任何要求 — 它作为安全标记。
在 Swift 5.5 之前,开发者可以通过 DispatchQueue 在线程之间传输任何对象,而编译器不会检查这是否安全。Sendable 封闭了这个漏洞:现在编译器自己跟踪线程间传输并阻止不安全的传输。这使得并发代码在运行之前就更加可靠。
struct UserProfile: Sendable {
let name: String
let age: Int
}
class NonSendableClass {
var counter: Int = 0
}
在示例中,UserProfile 可以安全地在线程之间传输,因为它是一个具有 Sendable 类型常量属性的结构体。NonSendableClass 在尝试通过 await 或 Task 传输时将引起编译错误。
Sendable 类型 在 Swift 中分为三类:具有 Sendable 属性的值类型、具有不可变状态的 final 类以及用 @Sendable 标记的函数/闭包。编译器为值类型自动推导 Sendable 合规性。
Struct、enum 和 tuple 如果所有属性和关联值也都是 Sendable,则自动成为 Sendable。这是一种保守方法:如果即使只有一个属性不符合 Sendable,整个类型将不会被视为安全。
| 类型 | Sendable | 条件 |
|---|---|---|
| Struct | 是(无需显式指示) | 所有属性 Sendable |
| Enum | 是(无需显式指示) | 所有关联值 Sendable |
| Final class | 是(需要显式指示) | 所有 let 属性 Sendable,无 var |
| Non-final class | 否 | 因继承原因无法成为 Sendable |
Swift 的所有内置类型 — Int、String、Double、Bool、Optional、Array、Dictionary、Set — 都符合 Sendable。这使得类型组合默认是安全的。开发者只需关注自定义类。
@unchecked Sendable — 是一种机制,允许类显式声明自己为 Sendable,绕过编译器的检查。开发者承担此类的线程安全性责任。这对于 Objective-C 桥接和优化结构很有用。
@unchecked Sendable 在类内部通过锁或原子操作保证安全性时应用,但编译器无法静态检查此事。例如,一个含有 os_unfair_lock 或 pthread_mutex_t 的类 — 其线程安全性由代码保证,但 Swift 无法检测到。
final class AtomicCounter: @unchecked Sendable {
private var value: Int = 0
private let lock = NSLock()
func increment() {
lock.lock()
value += 1
lock.unlock()
}
}
请谨慎使用 @unchecked Sendable。这是给其他开发者的一个清晰信号:“我已经检查过,这是安全的”。实现中的错误可能导致难以追踪的竞争条件。在应用 @unchecked 之前,请确保该类型确实无法重写为值类型。
Actor 和 Sendable 是同一枚硬币的两面。Actor 隔离其状态,但与外界交换数据时必须返回 Sendable 类型。如果 actor 方法返回非 Sendable 类型,编译器将发出警告或错误。
当 actor 向外部代码发送数据时,这些数据越过了隔离边界。Sendable 保证接收方可以在 actor 外部安全地使用它们。Actor 本身保持隔离 — 其内部状态不会暴露。
struct AccountSnapshot: Sendable {
let id: UUID
let balance: Double
let lastUpdated: Date
}
actor BankActor {
private var balance: Double = 0
func snapshot() async -> AccountSnapshot {
return AccountSnapshot(
id: UUID(),
balance: balance,
lastUpdated: Date()
)
}
}
AccountSnapshot — 是一个 Sendable 结构体,只包含 Sendable 类型的 let 属性。这种方法是从 actor 提取数据的最佳实践。状态快照按值传输,actor 不会失去对其状态的控制。
@Sendable — 是用于函数和闭包的属性,确保闭包不以可变方式捕获非 Sendable 数据。当闭包被传递到 Task 或 actor 方法时,它必须是 Sendable。
编译器检查 @Sendable 闭包是否捕获了可变引用。允许捕获 Sendable 类型的 let 属性。捕获引用类型的 var 变量将引起错误,因为闭包可能与变更同时执行。
func performAsync(operation: @Sendable () -> Void) {
Task {
await operation()
}
}
let constant = "Safe"
var counter = 0
// counter — 捕获 var 会变更计数器,编译错误
performAsync { // ❌ 已捕获 var 的变更
print(constant)
}
规则很简单:@Sendable 闭包只能捕获那些本身是Sendable且不会被外部变更的数据。对于类,如果类未标记为 Sendable,则允许捕获 weak self 弱引用。这防止了经典的引用循环和竞争条件。
常见问题
Sendable — 是一个标记,告诉编译器:“这个类型可以在线程之间安全地传输”。结构体和常量通常默认是 Sendable,类则不是,除非显式指定。
值类型(struct、enum)具有 Sendable 属性、具有不可变状态的 final 类以及所有 Swift 基础类型:Int、String、Double、Bool、Array、Dictionary、Optional — 自动符合 Sendable。
@unchecked Sendable — 是一种无需编译器静态检查就声明类为 Sendable 的方法。开发者自行保证安全性,通常通过锁或原子操作。
Actor 隔离其状态,但当数据返回给外部代码时,这些数据越过了隔离边界。Sendable 保证接收方在 actor 外部使用这些数据时不会遇到竞争条件。
将 Sendable 协议添加到一个 final 类,其所有属性都是 Sendable 类型的常量。如果类使用锁,可以应用 @unchecked Sendable,但这需要谨慎。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。