现象:看起来"随机"的结果顺序
在 Swift 并发模型里,withTaskGroup 让我们可以一次性启动多个子任务并发执行。
很多初学者第一次写出的代码类似下面这样
swift
import Foundation
/// 模拟网络请求:根据 id 返回字符串,耗时 0~4 秒随机
func fetchData(id: Int) async -> String {
// 让任务随机"卡"一会儿
try! await Task.sleep(for: .seconds(Int.random(in: 0..<5)))
return "Result for \(id)"
}
let results = await withTaskGroup(of: String.self) { group in
// 1. 把 0~5 共 6 个任务依次放进组里
for i in 0...5 {
group.addTask {
await fetchData(id: i)
}
}
// 2. 按任务完成顺序收集结果
var temp = [String]()
for await value in group {
temp.append(value)
}
return temp
}
print(results)
// 实际打印可能:["Result for 1", "Result for 5", "Result for 0", "Result for 2", "Result for 3", "Result for 4"]
运行后发现:
- 数组里字符串的下标和
for i in 0...5的循环顺序毫无关系。 - 哪个子任务先结束,哪个就排在前面------完全符合并发语义,却不符合"人类直觉"。
根本原因
TaskGroup 的 addTask 只是把任务扔进并发调度器,调度器按可用线程/协程资源自由执行。
for await ... in group 则是按完成顺序逐个给出结果。
因此"创建顺序"与"完成顺序"天然解耦,这是设计使然,不是 bug。
最通用、可扩展的修复思路
把"能用来排序的元数据"和"真正的结果"一起带回来。最常见的就是"下标/序号"本身。
修改后的核心代码:
swift
let ordered = await withTaskGroup(of: (Int, String).self) { group in
// 1. 返回元组:(原始下标, 业务结果)
for i in 0...5 {
group.addTask {
let value = await fetchData(id: i)
return (i, value) // 关键:把序号带回来
}
}
// 2. 先收集到字典(或临时数组)
var dict = [Int: String]()
for await (index, value) in group {
dict[index] = value
}
// 3. 按原始序号排序
return dict
.sorted(by: { $0.key < $1.key })
.map(\.value)
}
print(ordered)
// 保证是 ["Result for 0", "Result for 1", ... "Result for 5"]
知识点再梳理
withTaskGroup(of:)的of参数决定子任务返回类型。addTask闭包内部可以捕获外部常量,因此能拿到循环变量i。for await迭代的是完成顺序;想恢复"创建顺序"必须自带排序键。- 如果业务需要"部分结果优先返回",可以改用
AsyncSequence的merge()或TaskGroup+AsyncChannel
完整可运行 Demo
swift
import Foundation
/// 模拟网络请求
func fetchData(id: Int) async -> String {
let milliseconds = Int.random(in: 0..<5_000)
try! await Task.sleep(for: .milliseconds(milliseconds))
return "结果-\(id)"
}
/// 保证顺序的并行抓取函数
func fetchAll() async -> [String] {
await withTaskGroup(of: (Int, String).self) { group in
// 1. 添加任务
for i in 0...5 {
group.addTask {
let value = await fetchData(id: i)
return (i, value)
}
}
// 2. 收集
var dict = [Int: String](minimumCapacity: 6)
for await (index, value) in group {
dict[index] = value
}
// 3. 排序
return dict
.sorted(by: { $0.key < $1.key })
.map(\.value)
}
}
Task {
let list = await fetchAll()
print("最终顺序:", list)
}
总结与扩展场景
- 只要"对外表现需要有序",就一定把序号带回来;这是并发到顺序的通用模式,不限于 Swift。
- 如果子任务量巨大,占内存太多,可以把"结果"换成磁盘缓存 URL 或数据库主键,排序后再分批读取。
- 当顺序敏感且需要增量刷新 UI 时,改用
AsyncSequence并按序号插入List/UITableView数据源,用户体验更好。 - 若业务允许"先出来先展示",就无需任何额外工作,直接用
for await流式消费,反而性能最佳。
牢记:并发世界里,顺序不是默认,而是额外成本。想清楚"是否真的需要顺序",再决定要不要买单。