Sendable 解读
重点:(了解全貌,直到最后一刻..)
-
Swift 的 Sendable 是**针对类型(type)**的,而不是单个属性。
-
String、Int、Double、Bool、URL都 天然 Sendable,不需要你额外标记。 -
Sendable 的定义是:类型是否可以安全跨线程传递(跨隔离域传递)
-
Sendable 并不会阻止你对可变属性并发读写,它只保证"值本身是安全传递的"。
-
SO 关键点:Sendable 是"传递安全",不是"访问安全 (但这里还需要进一步区分..)"
Sendable 协议在 Swift 类型系统中的普适性:
Sendable 不仅仅用于标记闭包,它还可以标记任何你希望能在并发域(Task 或 Actor)之间安全传递或共享的类型。
简而言之,Sendable 可以标记:
-
类型 (Types): 结构体(Struct)、枚举(Enum)、类(Class)、Actor。
-
函数/闭包类型 (Function/Closure Types): 确保闭包本身及其捕获的行为是线程安全的。
🚀 Sendable 可以标记的五种主要目标
1. 结构体 (Structs) 和 枚举 (Enums)
如果一个值类型的所有存储属性或关联值都是 Sendable 的,那么该类型就自动满足 Sendable 协议。如果不是自动满足(例如,包含了非 Sendable 的属性),你可以显式地标记它。
swift
// 示例:一个结构体,如果其属性是 Sendable,则它自动是 Sendable
struct SafeConfiguration: Sendable {
let timeout: Double // Double 是 Sendable
let apiKey: String // String 是 Sendable
}
// 示例:一个枚举,如果其关联值是 Sendable,则它自动是 Sendable
enum ResultState<T: Sendable>: Sendable { // T 必须是 Sendable
case success(T)
case loading
case failure(Error) // Error 类型也是 Sendable 的
}
2. 类 (Classes)
对于引用类型(class),Sendable 的要求更严格,以防止多个引用同时修改共享状态。
-
Actor:
actor类型默认自动满足Sendable协议,因为 Actor 内置了对自身可变状态的隔离和同步机制。swiftactor DataCache: Sendable { // 自动满足 Sendable private var dataStore: [String: Data] = [:] } -
Final Class: 只有
final类才能满足Sendable协议。它要求:-
所有存储属性都是
Sendable的。 -
类不能被继承。
-
(如果需要)必须使用同步机制(如
NSLock)来保护可变属性。
-
swift
// 如果这个类内部使用锁来保护其可变状态
final class ThreadSafeCounter: @unchecked Sendable { // 假设内部有锁,所以使用 @unchecked Sendable
private var value = 0
private let lock = NSLock()
func increment() {
lock.lock()
defer { lock.unlock() }
value += 1
}
}
3. 函数和闭包类型 (Function/Closure Types)
正如你示例中看到的,@Sendable 是一个类型属性,用于修饰函数类型,确保闭包可以安全地跨越并发域。
swift
// 定义一个接受 Sendable 闭包的函数
func executeAsync(work: @Sendable () -> Void) {
// ... 将 work 提交到一个后台 Task 中
}
// block的示例:
let progressHandler: (@Sendable (Double) -> Void)?
4. 泛型类型约束 (Generic Type Constraints)
你可以使用 Sendable 作为泛型参数的约束,确保只有并发安全的数据类型才能被用在你的通用结构中。
swift
// 确保这个队列只能存储 Sendable 的元素
struct ConcurrentQueue<Element: Sendable>: Sendable {
// ...
}
5. 协议 (Protocols)
你可以使一个协议继承 Sendable,以强制要求所有遵循该协议的类型都必须是并发安全的。
swift
protocol ConcurrentModel: Sendable {
var id: String { get }
func validate() async throws
}
@unchecked Sendable 解读
swift
private final class UploadContext: @unchecked Sendable {
let continuation: CheckedContinuation<Data, Error>
let resumeKey: URL
let progressHandler: (@Sendable (Double) -> Void)?
init(continuation: CheckedContinuation<Data, Error>, resumeKey: URL, progressHandler: (@Sendable (Double) -> Void)? = nil) {
self.continuation = continuation
self.resumeKey = resumeKey
self.progressHandler = progressHandler
}
}
🟦 那么:为什么还要用 @unchecked Sendable?
因为 Swift 6 的 Sendable 检查会阻止:
-
class 传跨线程
-
actor 内部持有非 Sendable 成员
-
continuation 存在非 Sendable 上下文
-
delegate 回调流入 actor 时不安全的数据捕获
当你告诉 Swift:
"这个 class 是 Sendable,不要报错。"
"编译器别拦我,我自己负责确保这个引用类型不会被多个线程同时访问。"
这里👉,
@unchecked Sendable的作用不是为了让访问变安全,而是为了告诉编译器:
- "我知道它可能是不安全的,但我确保实际使用场景 不会有并发读写。"
🟩 那么问题的重点来了:
🟦 在 APIService(actor)中,这个 class 是否安全?
安全。100% 安全。
原因是:
✔ Actor 提供了"隔离":
所有对 UploadContext 的读写都发生在 同一个 actor 隔离域 内:
swift
actor APIService {
private var contexts: [Int: UploadContext]
// actor 内部,只有一个线程能访问
}
因此不会并发访问到同一个 UploadContext 实例。
重点:如果 class UploadContext 不加上 @unchecked Sendable ,无法在 actor APIService 中使用,直接报错⚠️⚠️
或者:
-
将 UploadContext 变成 struct(值类型) → 自动 Sendable
-
将 UploadContext 变成 actor(完全隔离) → 最安全
🟥 2. 为什么 UploadContext class 不加 @unchecked Sendable 会报错?
因为:
-
class是引用类型 -
Swift 的 Sendable 自动推断规则认为 class 不是 Sendable
-
你将它存入 actor 的属性中,例如:
swift
actor APIService {
private var uploadContexts: [Int: UploadContext] = [:] // ❌ 报错
}
报错:Type 'UploadContext' does not conform to 'Sendable'
那为什么要用 @unchecked Sendable?
因为:
-
你知道 UploadContext 只会在 actor 内部被访问
-
对它的所有读写都由 actor 的序列化执行保障
-
即使 class 是引用类型,也不会被多线程并发访问
-
因此它是逻辑上安全的
为什么是 @unchecked,而不是普通 Sendable?
因为 class 不能自动推断 Sendable
(引用类型可能被共享,可能同时被多个线程访问)
Sendable 协议要求:class 必须保证所有存储属性自身是 Sendable 且不会被并发访问`
这是不可能自动证明的。所以 Swift 要求你明确声明:
final class UploadContext: @unchecked Sendable
@unchecked 的意思:
-
我将在未来的使用中保证并发安全
-
你这个编译器还有你这个编译器统统脱掉裤子,我不检查(我不看)
-
风险自负
APIService 中这些方式会触发 Sendable 检查
- delegate 调用从 nonisolated → actor 的转换
swift
nonisolated func urlSession(...) {
Task { await self.handle(...) } // ⬅ 跨隔离
}
- final class UploadContext: @unchecked Sendable 被存入 actor 状态
swift
actor APIService {
var uploadContexts: [Int: UploadContext] // ⛔ 需要 Sendable
}
- continuation 是 Sendable 的,因此关联的上下文也要 Sendable
Continuation` 是 Swift 用来在 async/await 与传统回调之间"桥接"的对象,例如:
swift
withCheckedContinuation { continuation in
someCallbackAPI { result in
continuation.resume(returning: result)
}
}
CheckedContinuation)被设计成可以跨 actor 或线程发送,因此它本身遵守 Sendable
即:你可以把 continuation 存起来,之后在别的线程调用 resume()
那为什么"关联的上下文也要 Sendable"?
因为 如果一个类型里保存了 continuation,那这个类型整体也会被认为要跨线程传递。
例如:
swift
struct Wrapper {
let continuation: CheckedContinuation<Int, Never>
}
由于 continuation 是 Sendable,编译器认为 Wrapper 也需要是 Sendable。
但如果 Wrapper 里包含了一个非 Sendable 的东西,例如:
它不是一个纯正的美女,它具有女性的外表却还有男性的能力(Wow,Thai ladyboys....)
swift
class NotSendable {}
struct Wrapper {
let continuation: CheckedContinuation<Int, Never>
let obj: NotSendable(class)
}
这样编译器会报错:
csharp
❌ Wrapper is not Sendable because it stores a Sendable continuation with a non-Sendable value.
那么问题来了:Wrapper 是 class ??
swift
class Wrapper {
let continuation: CheckedContinuation<Int, Never>
}
如果你想在 Actor 内部存储它或作为参数传给 Actor,Swift 会报错:
swift
actor MyActor {
var wrapper: Wrapper? // ❌ 编译报错: Wrapper is not Sendable
}
正确姿势:(其实我们一般都喜欢更为混乱的姿势,666)
final class Wrapper: @unchecked Sendable {
let continuation: CheckedContinuation<Int, Never>
}
Therefore, correct posture is very important, including your's physical actions together, as it determines whether you achieve synchronized orgasms.
So... You know what I mean.