Swift并发任务拆分:DispatchQueue与Task的问题及性能对比
需要对数组中大量对象执行计算并将结果存入新数组,为提升速度拆分任务并发执行,用2秒等待模拟计算操作,分别尝试了DispatchQueue和Task两种方案:
1. DispatchQueue实现方案
代码如下:
import Foundation class Main { let originalData = ["a", "b", "c"] var calculatedData = Set<String>() func doCalculation() { //calculate length of array slices. let totalLength = originalData.count let sliceLength = Int(totalLength / 3) var start = 0 var end = 0 let myQueue = DispatchQueue(label: "Calculator", attributes: .concurrent) var allPartialResults = [Set<String>]() for i in 0..<3 { if i != 2 { start = sliceLength * i end = start + sliceLength - 1 } else { start = totalLength - sliceLength * (i - 1) end = totalLength - 1 } allPartialResults.append(Set<String>()) myQueue.async { allPartialResults[i] = self.doPartialCalculation(data: Array(self.originalData[start...end])) } } myQueue.sync(flags: .barrier) { for result in allPartialResults { self.calculatedData.formUnion(result) } } //do further calculations with the data } func doPartialCalculation(data: [String]) -> Set<String> { print("began") sleep(2) let someResultSet: Set<String> = ["some result"] print("ended") return someResultSet } }
运行情况:控制台日志符合预期(三个"began"同时输出,2秒后三个"ended"同时输出),真实场景下doCalculation()耗时从40ms降至约14ms。但为避免calculatedData的数据竞态,采用了每个子任务仅访问固定索引的部分结果数组方案,不够优雅;若尝试从并发队列调用主队列更新calculatedData,用sync会死锁,async会导致barrier失效。
2. Tasks实现方案
采用withTaskGroup实现,代码如下:
import Foundation class Main { let originalData = ["a", "b", "c"] var calculatedData = Set<String>() func doCalculation() async { //calculate length of array slices. let totalLength = originalData.count let sliceLength = Int(totalLength / 3) var start = 0 var end = 0 await withTaskGroup(of: Set<String>.self) { group in for i in 0..<3 { if i != 2 { start = sliceLength * i end = start + sliceLength - 1 } else { start = totalLength - sliceLength * (i - 1) end = totalLength - 1 } group.addTask { return await self.doPartialCalculation(data: Array(self.originalData[start...end])) } } for await newSet in group { calculatedData.formUnion(newSet) } } //do further calculations with the data } func doPartialCalculation(data: [String]) async -> Set<String> { print("began") try? await Task.sleep(nanoseconds: UInt64(1e9)) let someResultSet: Set<String> = ["some result"] print("ended") return someResultSet } }
运行情况:控制台日志显示任务串行执行(每个"ended"在对应"began"后2秒输出),耗时仍为40ms;真机运行替换sleep为Task.sleep后任务实现并发,但耗时仍高达40-50ms,偶尔超200ms,添加.userInitiated优先级无改善。
疑问
- 使用DispatchQueue时,如何从队列调用主队列避免数据竞态,同时保留后续的barrier标记功能?
- 使用Task时,如何真正实现并发执行?
- 为何相同并发操作下,Task比DispatchQueue耗时更长?
一、DispatchQueue方案:安全更新主队列数据并保留barrier功能
你当前的部分结果数组方案存在数据竞态风险(并发线程修改同一个数组),可以通过以下两种方式优化:
方式1:基于DispatchGroup收集结果,统一更新主队列
用DispatchGroup跟踪所有子任务完成状态,配合锁保护结果数组,最后在主队列同步更新calculatedData,避免barrier导致的线程阻塞:
func doCalculation() { let totalLength = originalData.count let sliceLength = totalLength / 3 let myQueue = DispatchQueue(label: "Calculator", attributes: .concurrent) let group = DispatchGroup() var allPartialResults = [Set<String>]() let resultLock = NSLock() // 保护结果数组的线程安全 for i in 0..<3 { let start: Int, end: Int if i != 2 { start = sliceLength * i end = start + sliceLength - 1 } else { start = totalLength - sliceLength * (i - 1) end = totalLength - 1 } group.enter() myQueue.async { defer { group.leave() } let result = self.doPartialCalculation(data: Array(self.originalData[start...end])) // 加锁写入结果数组 resultLock.lock() allPartialResults.append(result) resultLock.unlock() } } // 等待所有子任务完成,再更新主队列数据 group.notify(queue: .main) { for result in allPartialResults { self.calculatedData.formUnion(result) } // 执行后续计算逻辑 } }
方式2:保留barrier,在barrier块内安全更新主队列
如果你坚持使用barrier,可以在barrier同步块内合并结果后,异步提交到主队列更新calculatedData,避免死锁:
myQueue.sync(flags: .barrier) { // 先合并所有部分结果(此时所有子任务已完成) let merged = allPartialResults.reduce(into: Set<String>()) { $0.formUnion($1) } // 异步提交到主队列更新,不会阻塞当前线程 DispatchQueue.main.async { self.calculatedData.formUnion(merged) } }
注意:此方式需先确保allPartialResults的线程安全,建议用锁保护子任务对数组的写入操作。
二、Tasks方案:实现真正并发执行
你的代码任务串行的核心原因是闭包捕获变量错误,循环中的start和end是可变变量,闭包会捕获其引用,导致所有子任务使用的是最后一次循环的切片值。此外,还需注意线程安全和调度策略:
1. 修复闭包捕获问题
将循环内的start和end定义为let常量,确保每个子任务捕获独立的切片范围:
await withTaskGroup(of: Set<String>.self) { group in for i in 0..<3 { let start: Int, end: Int if i != 2 { start = sliceLength * i end = start + sliceLength - 1 } else { start = totalLength - sliceLength * (i - 1) end = totalLength - 1 } group.addTask(priority: .userInitiated) { return await self.doPartialCalculation(data: Array(self.originalData[start...end])) } } // 合并结果时注意线程安全,若calculatedData在多线程访问,需加锁或用Actor let lock = NSLock() for await newSet in group { lock.lock() self.calculatedData.formUnion(newSet) lock.unlock() } }
2. 优化CPU密集型任务的调度
如果doPartialCalculation是CPU密集型工作,建议用Task.detached创建脱离当前上下文的任务,避免抢占当前线程:
group.addTask { return await Task.detached(priority: .userInitiated) { return self.doPartialCalculation(data: Array(self.originalData[start...end])) }.value }
同时,确保doPartialCalculation内的工作是真正的异步或可抢占的,若为纯同步计算,可去掉async修饰,直接返回结果(Task会自动调度到线程池执行)。
三、Task比DispatchQueue耗时更长的原因
调度模型差异:
DispatchQueue基于内核级抢占式调度,线程由系统直接管理,调度开销低;而Swift Concurrency的Task是协作式抢占,依赖Runtime层调度,任务启动和切换有额外开销,小任务场景下更明显。线程池策略不同:
DispatchQueue的并发队列默认会根据系统负载动态调整线程数,而Swift Concurrency的全局线程池默认限制为CPU核心数(或核心数+1),对于CPU密集型任务,DispatchQueue能更快调度到空闲线程。优先级与QoS映射差异:
Task的优先级继承自上下文,若doCalculation在低优先级Task中调用,子任务会继承低优先级;而DispatchQueue默认QoS为.default,调度优先级更稳定。测量与系统干扰:
os_signpost对Task的测量会包含调度、挂起等额外步骤,导致耗时统计偏高;真机上系统进程的CPU抢占、内存压力等因素,也会导致Task的执行时间波动更大。线程安全开销:
Task方案中若未正确处理calculatedData的线程安全,隐式的同步操作会增加耗时;而DispatchQueue的barrier或显式锁的开销更可控。
内容的提问来源于stack exchange,提问作者Gary

