Swift中实现线程安全进度报告的合理方案咨询
Swift多并发场景下ProgressGroup的线程安全实现方案
问题背景
我在Swift代码库中使用Progress和ProgressGroup类跟踪长时操作的进度:
ProgressGroup包含多个子Progress对象,子Progress会被传入长时执行函数,调用setProgress(_:)即可更新自身进度。- 子进度更新后会触发
ProgressGroup的setChildProgress(_:progress:)方法,计算总进度(比如3个子进度为0.0、0.5、0.0时,总进度为0.16)。 ProgressGroup标记为@Observable,用于关联UI或其他跟踪模块。
原代码通过@MainActor隔离避免并发调用问题,但现在需要让Progress被不同actor的多并发任务调用:
- 移除
@MainActor隔离会引发竞态问题; - 使用
Mutex不符合Swift推荐的并发范式; - 将
ProgressGroup改为actor会产生大量actor实例,还会迫使整个调用链异步。
原代码示例:
@MainActor @Observable class ProgressGroup { var childProgress: [Float] var children: [Progress]! = [] var totalProgress: Float = 0.0 init (numChildren: Int) { self.childProgress = Array(repeating: 0.0, count: numChildren) self.children = (0..<numChildren).map { idx in Progress(group: self, index: idx) } } func setChildProgress(idx: Int, progress: Float) { self.childProgress[idx] = progress var total: Float = 0.0 for childProgress in self.childProgress { total += childProgress } self.totalProgress = total / Float(self.children.count) // triggers observers } } @MainActor class Progress { let group: ProgressGroup let index: Int init (group: ProgressGroup, index: Int) { self.group = group self.index = index } func setProgress(progress: Float) { self.group.setChildProgress(idx: self.index, progress: progress) } }
可行解决方案
方案1:自定义串行执行器(SerialExecutor)
给ProgressGroup指定专用的串行执行器,替代主actor,既保证线程安全,又不会阻塞主线程,也无需将整个类改为actor。
代码示例:
import Foundation import Observation // 定义专用串行执行器 private let progressGroupExecutor = SerialExecutor { task in let queue = DispatchQueue(label: "com.yourdomain.progressgroup") queue.async(execute: task) } @Observable class ProgressGroup { var childProgress: [Float] var children: [Progress]! = [] var totalProgress: Float = 0.0 // 指定执行器 nonisolated let executor: SerialExecutor = progressGroupExecutor init(numChildren: Int) { self.childProgress = Array(repeating: 0.0, count: numChildren) self.children = (0..<numChildren).map { idx in Progress(group: self, index: idx) } } func setChildProgress(idx: Int, progress: Float) { guard idx >= 0, idx < childProgress.count else { return } childProgress[idx] = progress let total = childProgress.reduce(0.0, +) totalProgress = total / Float(childProgress.count) } } class Progress { let group: ProgressGroup let index: Int init(group: ProgressGroup, index: Int) { self.group = group self.index = index } nonisolated func setProgress(progress: Float) { // 自动切换到ProgressGroup的串行执行器执行更新 Task { [group, index] in await group.setChildProgress(idx: index, progress: progress) } } }
说明:所有对setChildProgress的调用都会在专用串行队列上执行,避免竞态。Progress可在任意actor调用setProgress,内部通过Task自动切换执行上下文,无需调用方强制等待。
方案2:用ActorIsolated封装状态
将ProgressGroup的核心状态(子进度数组、总进度)用ActorIsolated封装,通过非隔离方法暴露更新接口,既保证线程安全,又无需手动管理actor。
代码示例:
import Foundation import Observation @Observable class ProgressGroup { // 用ActorIsolated封装状态,自动保证线程安全 private let _childProgress: ActorIsolated<[Float]> private let _totalProgress: ActorIsolated<Float> var children: [Progress]! = [] // 对外暴露的总进度属性,自动同步内部隔离状态 var totalProgress: Float { get { Task { await _totalProgress.value }.value } set { Task { await _totalProgress.setValue(newValue) }.value } } init(numChildren: Int) { self._childProgress = ActorIsolated(Array(repeating: 0.0, count: numChildren)) self._totalProgress = ActorIsolated(0.0) self.children = (0..<numChildren).map { idx in Progress(group: self, index: idx) } } nonisolated func setChildProgress(idx: Int, progress: Float) { Task { await _childProgress.withValue { childProgress in guard idx >= 0, idx < childProgress.count else { return } childProgress[idx] = progress let total = childProgress.reduce(0.0, +) await _totalProgress.setValue(total / Float(childProgress.count)) } } } } class Progress { let group: ProgressGroup let index: Int init(group: ProgressGroup, index: Int) { self.group = group self.index = index } nonisolated func setProgress(progress: Float) { group.setChildProgress(idx: index, progress: progress) } }
说明:ActorIsolated会自动用隐藏的actor管理状态,所有状态修改都在隔离环境中执行,外部调用无需处理异步逻辑,@Observable能正常感知totalProgress的变化。
方案3:GCD串行队列保护状态
如果习惯传统GCD同步方式,用串行队列封装状态访问也是官方认可的方案,适合对actor模型不熟悉的场景。
代码示例:
import Foundation import Observation @Observable class ProgressGroup { // 专用串行队列,保护状态访问 private let queue = DispatchQueue(label: "com.yourdomain.progressgroup") private var _childProgress: [Float] private var _totalProgress: Float = 0.0 var children: [Progress]! = [] // 对外暴露的总进度,通过队列同步获取最新值 var totalProgress: Float { queue.sync { _totalProgress } } init(numChildren: Int) { self._childProgress = Array(repeating: 0.0, count: numChildren) self.children = (0..<numChildren).map { idx in Progress(group: self, index: idx) } } func setChildProgress(idx: Int, progress: Float) { queue.async { [weak self] in guard let self = self, idx >= 0, idx < self._childProgress.count else { return } self._childProgress[idx] = progress let total = self._childProgress.reduce(0.0, +) self._totalProgress = total / Float(self._childProgress.count) // @Observable属性需在MainActor更新以触发UI观察 Task { @MainActor in self.totalProgress = self._totalProgress } } } } class Progress { let group: ProgressGroup let index: Int init(group: ProgressGroup, index: Int) { self.group = group self.index = index } func setProgress(progress: Float) { group.setChildProgress(idx: index, progress: progress) } }
说明:所有状态修改都在串行队列中执行,避免竞态。由于@Observable的UI观察依赖MainActor,最后通过@MainActor的Task更新对外暴露的totalProgress属性。
方案选择建议
- 若追求Swift现代并发范式,优先选方案1或方案2:方案1的串行执行器更灵活,可自定义执行队列;方案2用
ActorIsolated封装更简洁,无需手动管理执行上下文。 - 若习惯GCD同步方式,可选用方案3,兼容性好,学习成本低。
内容的提问来源于stack exchange,提问作者tmlen
相关产品推荐
相关产品推荐

