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

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耗时更长的原因

  1. 调度模型差异:
    DispatchQueue基于内核级抢占式调度,线程由系统直接管理,调度开销低;而Swift Concurrency的Task是协作式抢占,依赖Runtime层调度,任务启动和切换有额外开销,小任务场景下更明显。

  2. 线程池策略不同:
    DispatchQueue的并发队列默认会根据系统负载动态调整线程数,而Swift Concurrency的全局线程池默认限制为CPU核心数(或核心数+1),对于CPU密集型任务,DispatchQueue能更快调度到空闲线程。

  3. 优先级与QoS映射差异:
    Task的优先级继承自上下文,若doCalculation在低优先级Task中调用,子任务会继承低优先级;而DispatchQueue默认QoS为.default,调度优先级更稳定。

  4. 测量与系统干扰:
    os_signpost对Task的测量会包含调度、挂起等额外步骤,导致耗时统计偏高;真机上系统进程的CPU抢占、内存压力等因素,也会导致Task的执行时间波动更大。

  5. 线程安全开销:
    Task方案中若未正确处理calculatedData的线程安全,隐式的同步操作会增加耗时;而DispatchQueue的barrier或显式锁的开销更可控。


内容的提问来源于stack exchange,提问作者Gary

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 12:48:25