You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 14:35:35