非隔离上下文中的并发修改,编译时即会报错:
var externalVariable = 0
Task {
externalVariable += 1 // 🚨 可能报错!
}
您指出的这个例子非常关键!让我详细解释为什么这种代码在 Swift 并发模型中会报错。
问题根源:可变状态的并发访问
var externalVariable = 0
Task {
externalVariable += 1 // 🚨 可能报错!
}
这个代码会触发编译错误,原因如下:
1. 捕获可变变量(Capturing Mutable Variables)
当您在 Task 闭包中引用外部变量 externalVariable 时,Swift 会捕获这个变量。问题在于:
-
externalVariable是一个var(可变变量) -
Task可能在不同的线程上执行 - 如果有多个
Task同时捕获并修改同一个变量,就会产生数据竞争
编译器检测到可变状态可能在并发上下文中被访问,从而可能产生数据竞争。Swift 的并发模型要求所有跨并发边界共享的可变状态必须通过安全机制(如 Actor)来保护。
2. 编译器如何检测这个问题
Swift 编译器通过 Sendable 协议 来检查并发安全性。
// 编译器看到的逻辑:
var externalVariable = 0 // 这是一个可变的本地位变量
Task { // 这个闭包可能在任意线程上执行
// 这里捕获了 externalVariable
externalVariable += 1 // 🚨 危险!可能与其他线程同时修改
}
编译器会检查:
- 被捕获的变量是否是
Sendable的 - 闭包本身是否是
@Sendable的
具体的错误场景
场景1:多个 Task 修改同一个变量
func problematicExample() {
var counter = 0
// 启动多个并发任务
for i in 1...5 {
Task {
// 所有 Task 都捕获并修改同一个 counter 变量
counter += i // 🚨 编译错误!
print("Counter: \(counter)")
}
}
}
编译器错误信息可能类似:
Reference to captured var 'counter' in concurrently-executing code
场景2:即使是单个 Task 也可能报错
func singleTaskExample() {
var message = "Hello"
Task {
message += " World!" // 🚨 可能也会报错!
print(message)
}
}
为什么单个 Task 也会报错?因为编译器无法确定:
- 这个 Task 是否真的只会运行一次
- 外部代码是否会在 Task 执行期间也修改
message - 未来代码修改是否会引入并发访问
正确的解决方案
方案1:使用值类型(Value Types)
// 使用 let 声明不可变变量
func solution1() {
let greeting = "Hello" // 使用 let,不可变
Task {
let newGreeting = greeting + " World!" // ✅ 安全,创建新值
print(newGreeting)
}
}
方案2:使用 Actor 保护共享状态
actor Counter {
private var value = 0
func increment(by amount: Int) -> Int {
value += amount
return value
}
}
func solution2() async {
let counter = Counter()
await withTaskGroup(of: Void.self) { group in
for i in 1...5 {
group.addTask {
let result = await counter.increment(by: i) // ✅ 安全
print("Result: \(result)")
}
}
}
}
方案3:使用 @MainActor 进行 UI 更新
class ViewModel: ObservableObject {
@Published var count = 0 // 被 @MainActor 保护
func updateCount() {
Task { @MainActor in
self.count += 1 // ✅ 安全,在 MainActor 上执行
}
}
}
方案4:使用线程安全的引用类型
final class ThreadSafeContainer: @unchecked Sendable {
private let lock = NSLock()
private var _data: String = ""
var data: String {
get {
lock.lock()
defer { lock.unlock() }
return _data
}
set {
lock.lock()
defer { lock.unlock() }
_data = newValue
}
}
}
func solution4() {
let container = ThreadSafeContainer()
Task {
container.data = "New value" // ✅ 安全,内部有锁保护
}
}
Swift 并发模型的目标是:
- 编译时安全:在代码编译阶段就发现并发问题
- 无数据竞争:确保运行时的代码不会出现难以调试的并发 bug
- 性能可预测:避免运行时锁的开销和死锁风险
对比其他语言
- Dart:通过 Isolate 的物理隔离,根本上避免这个问题
- Go:依赖开发者的纪律,编译器不进行严格的并发安全检查
-
Java:运行时可能抛出
ConcurrentModificationException,但不是编译时错误 - Rust:通过所有权系统在编译时防止数据竞争,与 Swift 的理念相似
什么是 Sendable 协议?
Sendable 是一个标记协议(marker protocol),它本身不要求实现任何方法。它的唯一作用是告诉编译器:"这个类型的实例可以安全地在并发域之间传递"。
protocol Sendable { }
// 没有方法要求,只是一个标记
为什么需要 Sendable?
在 Swift 并发模型中,当数据在不同的并发域(如不同的 Task 或 Actor)之间传递时,编译器需要确保这种传递是线程安全的。Sendable 就是这种安全性的"证书"。
问题场景回顾
class NotThreadSafe {
var data: String = "Hello"
}
func problematicExample() {
let instance = NotThreadSafe()
Task {
instance.data = "World" // 🚨 潜在的数据竞争!
}
}
这里,NotThreadSafe 的实例在多个并发域间共享,但编译器无法保证它的线程安全性。
Sendable 的类型分类
1. 隐式 Sendable 类型(编译器自动推断)
许多基础类型天生就是线程安全的,编译器会自动将它们标记为 Sendable:
// 这些类型隐式符合 Sendable
let string: String = "Hello" // ✅ Sendable
let int: Int = 42 // ✅ Sendable
let double: Double = 3.14 // ✅ Sendable
let bool: Bool = true // ✅ Sendable
let array: [String] = ["a", "b"] // ✅ 如果元素是 Sendable
let dictionary: [String: Int] = [:] // ✅ 如果键值都是 Sendable
let optional: String? = nil // ✅ 如果包装类型是 Sendable
2. 值类型(结构体、枚举)
值类型通常自动符合 Sendable,因为它们在传递时会被复制:
struct Point: Sendable { // ✅ 编译器自动符合 Sendable
var x: Double
var y: Double
}
enum Status: Sendable { // ✅ 编译器自动符合 Sendable
case idle
case processing(progress: Double)
case finished(result: String)
}
例外情况:如果值类型包含非 Sendable 的引用类型:
class NotSendableClass { // 🚨 非 Sendable
var data: String = ""
}
struct ProblematicStruct { // 🚨 不会自动符合 Sendable
var point: Point // ✅ Sendable
var reference: NotSendableClass // 🚨 非 Sendable
}
3. 引用类型(类)
类默认不符合 Sendable,因为多个引用可能指向同一个实例:
class User { // 🚨 默认不符合 Sendable
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
要让类符合 Sendable,必须满足严格条件:
// 方式1:不可变类
final class ImmutableUser: Sendable { // ✅ 符合 Sendable
let name: String // 所有属性都是 let
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
// 方式2:内部同步的类
final class ThreadSafeCounter: Sendable { // ✅ 符合 Sendable
private var count: Int = 0
private let lock = NSLock() // 内部使用锁保护
func increment() {
lock.lock()
defer { lock.unlock() }
count += 1
}
func getCount() -> Int {
lock.lock()
defer { lock.unlock() }
return count
}
}
Sendable 的实际应用场景
场景1:Task 间传递数据
func sendDataBetweenTasks() async {
let safeData = SafeData(value: 100) // 假设 SafeData 符合 Sendable
let task1 = Task {
// 可以安全地使用 safeData
print("Task 1: \(safeData.value)")
}
let task2 = Task {
// 也可以安全地使用
print("Task 2: \(safeData.value)")
}
await (task1.value, task2.value)
}
场景2:Actor 方法参数和返回值
actor DataProcessor {
// 参数和返回值必须是 Sendable 的
func process(_ data: SafeData) async -> ProcessedResult { // ✅
// 处理逻辑
}
// 🚨 如果使用非 Sendable 类型会编译错误
// func processUnsafe(_ data: NotSendableClass) async { }
}
场景3:@Sendable 闭包
Task 接受的闭包默认是 @Sendable 的:
var counter = 0 // 在函数外部
func testSendableClosure() {
Task { // 这个闭包是 @Sendable
// 🚨 编译错误:不能捕获可变的外部变量
// counter += 1
// ✅ 可以捕获不可变的值
let localCopy = counter
print(localCopy)
}
}
编译器如何检查 Sendable?
Swift 编译器在以下几个时机检查 Sendable 符合性:
1. 跨并发域传递时
class NotSendable { }
func test() {
let instance = NotSendable()
Task {
// 🚨 编译错误:Capturing non-Sendable type 'NotSendable'
useInstance(instance)
}
}
func useInstance(_ instance: NotSendable) { }
2. 泛型约束中
func process<T: Sendable>(_ value: T) async { // 要求 T 是 Sendable
// 可以在并发环境中安全使用 value
}
class NonSendableClass { }
func testGeneric() async {
let safe = SafeData(value: 42)
await process(safe) // ✅ SafeData 符合 Sendable
let unsafe = NonSendableClass()
// await process(unsafe) // 🚨 编译错误
}
3. 协议关联类型中
protocol Repository {
associatedtype Item: Sendable // 要求关联类型是 Sendable
func getItem() async -> Item
}
处理非 Sendable 类型
如果必须使用非 Sendable 类型,有几种解决方案:
方案1:使用 @unchecked Sendable
final class UnsafeButMarked: @unchecked Sendable {
var data: String = ""
// 开发者自己保证线程安全
// 但编译器不再检查!
}
警告:这相当于告诉编译器"相信我",需要开发者自己确保线程安全。
方案2:在 Actor 内部封装
actor DataContainer {
private var unsafeData: NotSendableClass // 只在 Actor 内部访问
func safeOperation() async -> String {
// 安全的操作,由 Actor 隔离保证
return unsafeData.someProperty
}
}
方案3:转换为 Sendable 的值类型
struct SafeSnapshot {
let value: String
let timestamp: Date
}
func createSnapshot(from unsafe: NotSendableClass) -> SafeSnapshot {
return SafeSnapshot(
value: unsafe.data,
timestamp: Date()
)
}
总结
Sendable 协议是 Swift 并发安全的基石:
- 标记作用:标识类型可以安全跨并发域传递
- 编译时检查:在编译阶段防止数据竞争
- 自动推断:值类型和不可变类通常自动符合
- 强制安全:要求开发者显式处理线程安全问题
这种设计让 Swift 能够在共享内存的并发模型中,提供接近 Dart Isolate 隔离模型的安全性,同时保持更好的性能和灵活性。