1. 什么是 Sendable
?
#
Sendable
是 Swift 并发模型 引入的一个协议,用于确保类型在多个线程或并发上下文中传递时是 线程安全 的。它的核心作用是:
- 标记数据是否可以安全地在任务、线程或 Actor 之间共享。
- 防止数据竞争条件,提升代码的并发可靠性。
struct User: Sendable {
let name: String
let age: Int
}
在这个例子中,User
结构体是不可变的(let
属性),因此它天然是线程安全的,符合 Sendable
要求。
2. 为什么需要 Sendable
?
#
在 Swift 并发中,任务可能会在不同线程之间调度。为了避免数据竞争和内存访问冲突,编译器需要确保:
- 数据在传递到不同并发上下文时是安全的。
- 确保 Actor 的内部状态不会因为并发访问而出现竞态条件。
如果未正确标记或实现 Sendable
,编译器会在并发代码中发出警告或错误,帮助开发者提前发现潜在的线程安全问题。
3. Sendable
的使用场景
#
✅ 3.1 在 Actor 中共享数据 #
actor Counter {
private var value = 0
func increment() {
value += 1
}
}
struct Logger: Sendable {
let message: String
}
let counter = Counter()
Task {
await counter.increment()
print(Logger(message: "计数器已增加"))
}
Logger
是不可变的,因此可以标记为Sendable
,安全地跨任务使用。actor
会自动保护内部状态,但与外界的数据交互时仍需确保数据是Sendable
。
✅ 3.2 在 Task
中传递数据
#
struct DataModel: Sendable {
let id: Int
let name: String
}
let data = DataModel(id: 1, name: "并发数据")
Task.detached {
print(data) // ✅ 安全,因为 DataModel 符合 Sendable
}
Task.detached
是独立于当前上下文的任务,尤其强调线程安全。- 编译器会强制要求传入的数据是
Sendable
,否则会警告。
✅ 3.3 自定义类的线程安全 #
final class Config: Sendable {
private let lock = NSLock()
private var _value: Int = 0
var value: Int {
get {
lock.lock()
defer { lock.unlock() }
return _value
}
set {
lock.lock()
_value = newValue
lock.unlock()
}
}
}
- 类默认不符合
Sendable
,因为类实例通常是引用类型,可能导致数据竞争。 - 如果要标记为
Sendable
,必须自己确保线程安全,比如使用NSLock
保护。
4. 何时必须使用 Sendable
?
#
场景 | 是否必须 Sendable | 解释 |
---|---|---|
Actor 内部状态共享 | ✅ 必须 | 防止竞态条件,确保数据安全。 |
Task.detached 中的数据传递 | ✅ 必须 | 因为任务脱离父上下文,线程安全尤为重要。 |
跨线程传递数据 | ✅ 必须 | 保证数据在不同线程间的安全。 |
简单的 Task 或 async 函数 | ❌ 不强制,但推荐 | 如果没有跨线程访问,编译器不会强制要求。 |
引用类型(类) | ❌ 默认不符合,需手动确保线程安全 | 需使用锁机制或设计为不可变类。 |
5. Sendable
自动符合的类型
#
Swift 中很多基础类型天然符合 Sendable
,因为它们是不可变或具备值语义:
- 原始数据类型:
Int
、Double
、Bool
、String
等 - 集合类型:
Array
、Dictionary
(前提是元素也符合Sendable
) - 枚举类型: 如果所有关联值都符合
Sendable
,则枚举自动符合 - 结构体: 所有属性都符合
Sendable
,则结构体自动符合
6. 自定义类型如何手动符合 Sendable
#
✅ 简单结构体自动符合: #
struct Point: Sendable {
let x: Int
let y: Int
}
因为 x
和 y
都是 Int
,自动符合 Sendable
。
⚠️ 引用类型手动实现: #
final class Counter: @unchecked Sendable {
private var value = 0
func increment() {
value += 1 // 需要开发者确保线程安全
}
}
- 使用
@unchecked Sendable
表示你承诺线程安全,编译器不再强制检查。 - 风险: 开发者需要自行确保内部实现的线程安全,否则可能导致隐藏的竞态条件。
7. 常见问题与陷阱 #
❓ 1. 为什么我的代码会提示 Sendable
警告?
#
- 可能因为你在并发上下文中传递了一个不符合
Sendable
的类型,比如引用类型的对象。 - 解决方案:检查数据结构,确保其属性是不可变的,或使用锁保护可变状态。
❓ 2. 一定要加 Sendable
吗?
#
- 不一定。 如果代码只在单线程或没有跨并发边界,
Sendable
并不是强制的。 - 但推荐: 在任何可能的并发场景下使用
Sendable
,即使编译器不报错,这是一种良好的编程习惯。
❓ 3. 为什么类默认不符合 Sendable
?
#
- 因为类是引用类型,可能被多个任务共享,容易产生竞态条件。
- 如果要标记为
Sendable
,必须明确确保线程安全,比如使用NSLock
、DispatchQueue
等机制。
8. 总结 #
Sendable
是 Swift 并发的线程安全基石。- 在并发场景下,确保数据可以安全地在任务、线程和 Actor 之间传递。
- 使用
@unchecked Sendable
需要格外小心,确保线程安全。 - 推荐在设计并发模型时,尽量使用不可变的数据结构,这样天然符合
Sendable
,减少出错概率。