Swift 结构化并发中的 TaskGroup 能够同时启动多个子任务,并在一个作用域内统一等待它们产生结果。不过,很多开发者容易把提交顺序和完成顺序混为一谈:addTask 按照代码书写顺序把任务加入分组,但每个任务实际执行时长不同,先提交的任务不一定先完成。如果业务上要求“谁先完成谁先展示”,直接把分组当作数组去读取会得到错误结果。本文重点讨论如何利用 TaskGroup 自身的 next 机制按实际完成顺序收集结果,以及如何在收集过程中保留原始索引、处理取消和错误。

TaskGroup的提交顺序与完成顺序有何不同
在 TaskGroup 中,提交顺序由 group.addTask 的调用先后决定。假设我们依次添加五个子任务,每个任务内部执行不同时长的休眠,那么它们的完成时间将取决于各自延迟的大小,而不是它们在代码中的位置。这种差异在批量下载、异步渲染、多接口聚合等场景中非常常见:用户希望先看到最快返回的内容,而不是先发出的请求结果。
如果使用 for await result in group 直接迭代,Swift 会在内部反复调用 group.next(),每次返回的是当前已经完成的那一个任务结果,而不是提交顺序。这一点与数组遍历完全不同,数组遍历是严格按下标顺序,而 TaskGroup 遍历是按运行时完成事件驱动的。理解这个底层行为,是按完成顺序收集结果的第一步。
下面用一段模拟代码来验证。五个任务分别休眠 500 毫秒、100 毫秒、800 毫秒、200 毫秒和 300 毫秒,提交序号固定为 1 到 5,打印完成顺序时可以看到序号不再是 1、2、3、4、5。
import Foundation
func fetchTaskGroupOrderDemo() async {
let start = Date()
await withTaskGroup(of: (Int, String).self) { group in
for index in 1...5 {
group.addTask {
let delays = [500, 100, 800, 200, 300]
let delay = UInt64(delays[index - 1]) * 1_000_000
try? await Task.sleep(nanoseconds: delay)
let elapsed = Date().timeIntervalSince(start)
return (index, "任务\(index) 完成于 \(String(format: "%.2f", elapsed)) 秒")
}
}
var finishedResults: [(Int, String)] = []
while let result = await group.next() {
finishedResults.append(result)
}
print("完成顺序结果:")
for item in finishedResults {
print("提交序号 \(item.0): \(item.1)")
}
}
}
Task {
await fetchTaskGroupOrderDemo()
}
执行后会发现第二个任务最先出现在结果数组中,然后是第四个、第五个、第一个、第三个。这说明 group.next() 的返回顺序就是完成顺序。接下来需要把这种特性应用到实际的数据收集策略中。
使用group.next()按完成顺序收集结果
要用 TaskGroup 实现按完成顺序排序结果,最简洁的方式是在添加完所有任务后,连续调用 await group.next(),把每次得到的结果追加到一个数组里。由于 next() 会挂起直到有任务完成,因此循环结束后的数组天然就是完成顺序,无需再进行额外排序。
与 for await 循环相比,显式调用 while let 循环的好处是可以在收集过程中插入额外的逻辑。例如,你可以记录每个任务完成时的时间戳,或者在收到第一个结果后提前更新 UI,而无需等待所有任务结束。这在需要增量渲染的列表场景中尤其有用。
func collectResultsInCompletionOrder() async {
await withTaskGroup(of: (Int, String).self) { group in
for index in 1...4 {
group.addTask {
let delay = UInt64([600, 150, 350, 80][index - 1]) * 1_000_000
try? await Task.sleep(nanoseconds: delay)
return (index, "资源\(index)加载完成")
}
}
var completed: [(Int, String)] = []
while let item = await group.next() {
completed.append(item)
print("当前完成:提交序号 \(item.0)")
}
// completed 数组已经是完成顺序
for item in completed {
print(item.1)
}
}
}
如果业务需要同时保留原来的提交顺序,可以让每个任务返回一个包含提交序号的元组,上面代码已经这样做了。收集完成后,既可以按完成顺序展示,也可以根据序号快速还原为提交顺序。这种“索引 + 值”的元组模式是 TaskGroup 结果收集中的常用技巧。
另一个细节是:所有任务的结果类型必须一致。如果不同任务的返回值类型不同,可以通过枚举封装,例如定义一个 TaskOutcome 枚举,关联不同类型的数据。这样仍能放入同一个 TaskGroup 中,并在收集时按完成顺序处理。
处理取消、错误与结果竞争
按完成顺序收集结果时,不能忽略任务取消和错误处理。Swift 的 TaskGroup 会在父任务取消时自动向子任务传播取消标记,子任务可以在执行过程中检查 Task.isCancelled 并提前退出。如果使用 ThrowingTaskGroup,当某个任务抛出错误时,try await group.next() 也会抛出该错误,此时整个收集过程会终止,尚未完成的任务会被取消。这是结构化并发的默认安全行为。
func collectWithThrowingGroup() async throws {
try await withThrowingTaskGroup(of: (Int, String).self) { group in
for index in 1...3 {
group.addTask {
if index == 2 {
throw URLError(.badServerResponse)
}
try await Task.sleep(nanoseconds: UInt64(index) * 300_000_000)
return (index, "数据\(index)")
}
}
var ordered: [(Int, String)] = []
while let item = try await group.next() {
ordered.append(item)
print("按完成顺序收到:\(item.0)")
}
}
}
如果希望在部分任务失败时仍然保留其他成功结果,可以改用返回 Result 类型的方式。不过 Swift 的 Result 带泛型参数,代码中写法较为繁琐,更推荐自定义一个枚举,让子任务无论成功失败都返回同一个枚举值。这样 next() 不会因为错误而中断,你可以按完成顺序处理成功和失败的结果。
关于结果竞争:由于 next() 是串行等待和收集,不会出现多个任务同时向数组写入的情况。但不要尝试在 group.addTask 的闭包里直接修改外部数组,那样会造成数据竞争。正确的做法是在分组完成后,统一用 next() 循环收集到局部数组,最后返回给外部。
完整示例:模拟网络请求优先展示
结合前面的讨论,这里给出一个相对完整的示例。假设我们需要批量请求三个远程资源,每个请求的延迟不同,希望在全部请求完成后,按实际完成顺序打印资源内容,并且不丢失提交序号。示例中使用了 Task.sleep 模拟网络延迟,实际项目里替换为 URLSession 的异步请求即可。
import Foundation
struct ResourceItem {
let requestIndex: Int
let content: String
}
func loadResourcesInCompletionOrder() async {
let start = Date()
let resources = await withTaskGroup(of: ResourceItem.self) { group in
let delays: [UInt64] = [800_000_000, 200_000_000, 500_000_000]
for index in 0...2 {
group.addTask {
try? await Task.sleep(nanoseconds: delays[index])
let elapsed = Date().timeIntervalSince(start)
return ResourceItem(
requestIndex: index,
content: "资源\(index)在 \(String(format: "%.2f", elapsed)) 秒返回"
)
}
}
var completedItems: [ResourceItem] = []
while let item = await group.next() {
completedItems.append(item)
}
return completedItems
}
print("按完成顺序展示资源:")
for item in resources {
print("提交序号 \(item.requestIndex):\(item.content)")
}
}
Task {
await loadResourcesInCompletionOrder()
}
运行这段代码时,第二个资源会先返回,然后是第三个、第一个。这说明实现策略生效。如果你需要把结果再转换回提交顺序,只需按照 requestIndex 排序即可;如果需要直接展示,则按数组原顺序展示即可。这种灵活性正是元组或结构体关联索引的价值所在。
最佳实践与替代方案对比
除了 TaskGroup,Swift 还提供了 async let 用于固定数量的并发任务。对于两三个已知任务,async let 写法更简单,但它的等待顺序是由代码中的 await 顺序决定的,如果想按完成顺序获取结果,需要手动记录每个任务的完成时间,再排序,灵活性不如 TaskGroup。因此,当需要同时处理大量动态数量的任务并且关注完成顺序时,TaskGroup 是更合适的选择。
另一个替代方案是使用 AsyncStream 自己维护一个完成事件流。每个子任务完成后向流中发送结果,外部消费者可以实时接收。但这种方案需要手动处理背压、取消和资源的释放,代码复杂度明显上升。除非需要非常细粒度的流式控制,否则直接使用 TaskGroup 自带的 next() 已经足够。
在实现按完成顺序收集结果的策略时,还应注意任务数量的控制。TaskGroup 并不会自动限制最大并发数,它会根据系统资源决定调度,但如果你一次性添加大量耗时任务,仍可能造成资源紧张。可以通过引入信号量或分批添加任务的方式控制并发峰值。另外,如果单个任务内部有多个阶段,建议在关键位置检查 Task.isCancelled,以便取消时能快速释放资源,避免无谓的等待。
Swift TaskGroup任务完成顺序结果排序修改时间:2026-08-25 06:00:00