Swift三种addTask用法差异、@Sendable作用及线程安全实现问询
问题背景及代码示例
以下是编写的Swift代码:
class App { private var value = 0 func start() async throws { await withTaskGroup(of: Void.self) { group in for _ in 1...100 { group.addTask(operation: self.increment) // 1 group.addTask { await self.increment() // 2 } group.addTask { self.value += 1 // 3 } } } } @Sendable private func increment() async { self.value += 1 // 4 } }
代码中第2、3行出现编译警告:Capture of 'self' with non-sendable type 'App' in a @Sendable closure。
启用Thread Sanitizer并移除第2、3行后,第4行出现ThreadSanitizer运行时警告:Swift access race in (1) suspend resume partial function for asyncTest.App.increment@Sendable () async -> () at 0x106003c80。
技术问题解答
1. 三种addTask调用方式的区别
- 方式1:直接传递
self.increment作为operation参数。由于increment被标记为@Sendable,符合addTask(operation:)对参数的@Sendable要求,因此不会触发编译警告。但increment内部对value的修改未做线程安全保护,仍存在数据竞争风险。 - 方式2:在闭包内调用
await self.increment()。Task Group的addTask要求闭包必须是@Sendable类型,该闭包会被隐式推断为@Sendable,但App类未遵循Sendable协议,捕获self时就会触发编译警告,提示非Sendable类型的实例被捕获到@Sendable闭包中。 - 方式3:在闭包内直接修改
self.value。同样,闭包被推断为@Sendable,但App不是Sendable类型,捕获self触发警告;同时直接读写value完全没有线程安全保护,数据竞争风险极高。
2. @Sendable属性的作用
@Sendable是Swift并发模型中的标记属性,用于标识闭包或函数可以安全地跨actor、线程传递并执行。- 它要求被标记的实体:要么是值类型(天然具备线程安全性),要么是遵循
Sendable协议的引用类型(需保证内部状态线程安全或为不可变引用);同时,实体内部捕获的变量也必须是Sendable类型,避免线程不安全的共享。 - 在Task Group、Task、Actor消息传递等并发场景中,Swift会强制要求传递的闭包/函数是
@Sendable,以此在编译阶段提前排查潜在的线程安全问题,降低运行时数据竞争的概率。
3. 让increment()函数实现线程安全的方法
有三种主流实现方式:
- 使用Actor:将
App改为Actor类型,Actor会自动保证内部状态的访问是串行的,从根源避免数据竞争。修改后代码如下:
actor App { private var value = 0 func start() async throws { await withTaskGroup(of: Void.self) { group in for _ in 1...100 { group.addTask(operation: self.increment) } } } private func increment() async { self.value += 1 } }
- 使用GCD串行队列:创建一个串行队列,将对
value的修改操作放入队列执行,保证同一时间只有一个线程访问value:
class App: Sendable { private var value = 0 private let queue = DispatchQueue(label: "com.example.valueQueue") func start() async throws { await withTaskGroup(of: Void.self) { group in for _ in 1...100 { group.addTask(operation: self.increment) } } } @Sendable private func increment() async { queue.sync { self.value += 1 } } }
- 使用锁:通过
NSLock或os_unfair_lock在访问value前后加锁、解锁,保证同一时间只有一个线程操作value:
class App: Sendable { private var value = 0 private let lock = NSLock() func start() async throws { await withTaskGroup(of: Void.self) { group in for _ in 1...100 { group.addTask(operation: self.increment) } } } @Sendable private func increment() async { lock.lock() defer { lock.unlock() } self.value += 1 } }
注意:使用GCD或锁的方式时,需要让App遵循Sendable协议,这样捕获self到@Sendable闭包中就不会出现编译警告。
内容的提问来源于stack exchange,提问作者97mik
相关产品推荐
相关产品推荐

