2-7.【Concurrency】如何在 TaskGroup 中收集所有子任务的返回值?

2 阅读2分钟

一、最标准、推荐的收集方式(for-await)

这是 官方推荐 + 最安全 的方式。

let results: [Int] = await withTaskGroup(of: Int.self) { group in
    for id in ids {
        group.addTask {
            await fetchValue(for: id)
        }
    }

    var collected: [Int] = []
    for await value in group {
        collected.append(value)
    }
    return collected
}

关键点

  • group.addTask → 并发执行

  • for await value in group串行、安全地消费结果

  • 返回时:

    • 所有子任务已完成
    • 不会有悬挂任务

二、结果顺序说明(非常重要 ⚠️)

TaskGroup 不保证返回顺序

addTask 顺序 ≠ 完成顺序 ≠ 收集顺序

for await value in group 的顺序是:

哪个子任务先完成,就先 yield 哪个结果

如果你需要“原始顺序”,看下面第 4 节。


三、收集 + 处理(流式消费)

如果你不只是收集,还要边收边处理:

await withTaskGroup(of: Int.self) { group in
    for job in jobs {
        group.addTask {
            await job.run()
        }
    }

    for await result in group {
        updateUI(result)   // 顺序执行,安全
    }
}

这是 TaskGroup 的黄金用法

  • 子任务并发
  • 结果处理串行
  • 无共享可变状态

四、保持原始顺序的收集方式

方法 1️⃣:返回 (index, value)

最常见 & 推荐

let results = await withTaskGroup(of: (Int, String).self) { group in
    for (index, id) in ids.enumerated() {
        group.addTask {
            (index, await fetch(id))
        }
    }

    var buffer = Array(repeating: "", count: ids.count)
    for await (index, value) in group {
        buffer[index] = value
    }
    return buffer
}

方法 2️⃣:预分配数组 + actor(不推荐)

actor Buffer {
    var values: [String]
    init(count: Int) {
        values = Array(repeating: "", count: count)
    }
}

let buffer = Buffer(count: ids.count)

await withTaskGroup(of: Void.self) { group in
    for (i, id) in ids.enumerated() {
        group.addTask {
            let v = await fetch(id)
            await buffer.set(i, v)
        }
    }
}

可行,但比方法 1 复杂,也更慢。


五、Throwing TaskGroup 的收集方式

如果子任务可能抛错:

let results = try await withThrowingTaskGroup(of: Int.self) { group in
    for id in ids {
        group.addTask {
            try await fetchValue(id)
        }
    }

    var collected: [Int] = []
    for try await value in group {
        collected.append(value)
    }
    return collected
}

行为说明

  • 任一子任务 throw:

    • 其余子任务 全部自动取消
  • for try await 会立刻抛出错误


六、只等全部完成,不关心结果?

如果你只是想“等它们跑完”:

await withTaskGroup(of: Void.self) { group in
    for job in jobs {
        group.addTask {
            await job.run()
        }
    }
    // 不需要 for await
}

退出作用域 = 所有子任务完成或取消。


七、常见坑(真的很常见)

❌ 1. 在子任务里写共享数组

var results: [Int] = []
withTaskGroup(of: Int.self) { group in
    group.addTask {
        results.append(await work()) // ❌ 数据竞争
    }
}

永远不要这么干


❌ 2. 在 group 外面等结果

var results: [Int] = []
await withTaskGroup(of: Int.self) { group in
    ...
}
print(results) // 这里可能是空的

结果应该 在 group 作用域内收敛并返回


八、速查总结(背这个就够)

在 TaskGroup 中收集子任务返回值的正确方式是:
withTaskGroup 作用域内,用 for await 串行消费 group 的结果。
TaskGroup 不保证顺序,如需顺序应显式携带 index。
永远不要在子任务中写共享可变状态。