首页 / Swift 编程语言教程 / 结构化并发:async let 与 TaskGroup

Swift 编程语言教程

结构化并发:async let 与 TaskGroup

本教程共 93 篇 · 第 81 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

SwiftSwift 编程语言教程并发结构化并发async letTaskGroup并行任务组

本节目标:用 async let 并行跑少量已知子任务,用任务组动态并行一批子任务并收集结果,理解结构化并发的父子生命周期保障。

上一章你学会了顺序地 await 一个异步函数。可有时候三件事互不依赖,完全可以同时跑、最后一起收结果。await 一个接一个等太慢了。Swift 的结构化并发就是干这个的:用 async let 和任务组(TaskGroup)把子任务组织起来,既并行又可控。

1-1 async let:并行启动已知子任务

普通 await 调用会等结果回来才往下走,子任务是串行的:

let first = await downloadPhoto(named: names[0])
let second = await downloadPhoto(named: names[1])
let third = await downloadPhoto(named: names[2])
let photos = [first, second, third]

三段下载一个接一个,总耗时是三者之和。但它们互不相干,完全能同时下。把 let 前面加上 async,就变成并行启动:

async let first = downloadPhoto(named: names[0])
async let second = downloadPhoto(named: names[1])
async let third = downloadPhoto(named: names[2])

let photos = await [first, second, third]

async let 那三行不写 await,所以代码不挂起,三个下载同时出发。直到 await [first, second, third] 这一行,程序才需要结果,于是停下等它们全部完成。总耗时约等于最慢的那一个,而不是三者之和。

Tip

判断用 await 还是 async let 的简单标准:后面的代码依赖这个结果吗?依赖就用 await(顺序);不依赖、只想稍后一起收,就用 async let(并行)。

1-2 async let 的两种典型用法

async let 适合”子任务数量和它们各自的逻辑在写代码时就确定了”的情况。比如同时拉取用户资料、好友列表、动态三条数据:

func loadDashboard() async {
    async let profile = fetchProfile()
    async let friends = fetchFriends()
    async let feed = fetchFeed()

    let (p, f, d) = await (profile, friends, feed)
    print("资料:\(p),好友数:\(f.count),动态:\(d.count)")
}

这里三个 async let 并行启动,最后一次性 await 把三个结果打包收回来。代码读起来依旧是线性、清爽的,没有回调套回调。

注意 await 可以作用在多个 async let 组成的元组上,一次性等齐。这是 async let 常见的收尾姿势。

1-3 任务组:动态数量的并行

async let 的短板是数量写死了。如果要下载的照片有几张、几十张,数量运行时才定,就得用任务组withTaskGroup 让你显式建一个组,往里动态加子任务:

func downloadAll() async {
    await withTaskGroup(of: Data.self) { group in
        let names = await listPhotos(inGallery: "暑假")
        for name in names {
            group.addTask {
                return await downloadPhoto(named: name)
            }
        }
        for await photo in group {
            show(photo)   // 每下载完一张就展示一张
        }
    }
}

withTaskGroup(of:)of 指定每个子任务返回的类型。循环里 addTask 往组里加子任务,它们并发执行。下面的 for await photo in group 按完成顺序逐个取出结果——注意不保证顺序,谁先好谁先来。

Note

任务组是”结构化”的核心体现:组的代码块结束前,Swift 保证所有子任务都跑完;父任务被取消,子任务也自动取消。你不会忘记等子任务,也不会漏掉取消,这正是结构化并发比手写线程安全的地方。

1-4 任务组收集结果到数组

上面的例子边收边展示,没存结果。如果想把全部结果攒成一个数组返回,就在循环里 append

let photos = await withTaskGroup(of: Data.self) { group in
    let names = await listPhotos(inGallery: "暑假")
    for name in names {
        group.addTask {
            return await downloadPhoto(named: name)
        }
    }
    var results: [Data] = []
    for await photo in group {
        results.append(photo)
    }
    return results
}

for await 每拿到一个子任务的结果就 append 进数组,等所有子任务结束,把数组作为整个任务组的返回值交出去。withTaskGroup 的返回值就是闭包里 return 的那个值。

1-5 会抛错的任务用 throwing 变体

如果子任务可能抛错,用 withThrowingTaskGroup

func downloadAllSafe() async throws -> [Data] {
    try await withThrowingTaskGroup(of: Data.self) { group in
        let names = await listPhotos(inGallery: "暑假")
        for name in names {
            group.addTask {
                return try await downloadPhotoThrowing(named: name)
            }
        }
        var results: [Data] = []
        for try await photo in group {
            results.append(photo)
        }
        return results
    }
}

注意区别:调用处多一个 try,循环里是 for try await。只要有一个子任务抛错,整个任务组会抛出,父任务能 do-catch 捕获。这是并发代码里处理错误的标准姿势。

1-6 结构化并发的四项保障

为什么 Swift 力推这种”结构化”写法,而不是让你随便开线程?因为父任务与子任务有明确的层级关系,带来四个实在好处:

第一,父任务不会漏等子任务——块结束前子任务必须全部完成。

第二,给子任务提了更高优先级,父任务的优先级会自动跟着提升。

第三,父任务被取消,所有子任务也自动取消,不会留下孤儿任务。

第四,任务本地值(task-local)能高效自动地传给子任务。

Tip

记住一句话:async let 是”隐式”创建的子任务,适合数量固定的场景;withTaskGroup 是”显式”创建并动态管理子任务,适合数量不定、需要更细控制(优先级、取消)的场景。两者都是结构化并发,都享受上面的保障。

1-7 async let 与任务组能混用

两种方式不是二选一,可以组合。比如先用 async let 拉取名称列表,再进任务组批量下载:

func buildGallery() async throws {
    async let names = listPhotos(inGallery: "暑假")
    let photos = try await withThrowingTaskGroup(of: Data.self) { group in
        for name in await names {
            group.addTask { try await downloadPhotoThrowing(named: name) }
        }
        var r: [Data] = []
        for try await p in group { r.append(p) }
        return r
    }
    print("共下载 \(photos.count) 张")
}

这里 async let names 先并行取列表,取回后喂给任务组去并发下载。两种结构在同一段逻辑里协作,是真实代码里常见的形态。

1-8 优先级与取消在任务组里怎么用

任务组不只是”并行”,还能精细控制。给子任务提优先级、在取消后不再加新任务,都是常见刚需。

addTask 可以带 priority 参数,告诉系统这个子任务多紧急;如果父任务优先级被提升,子任务也会跟着升。取消方面,与其盲目 addTask,不如用 addTaskUnlessCancelled:一旦组已被取消,它直接返回 false、不再启动新工作。

await withTaskGroup(of: Data?.self) { group in
    for name in names {
        let added = group.addTaskUnlessCancelled {
            Task.isCancelled ? nil : await downloadPhoto(named: name)
        }
        guard added else { break }   // 已取消就停止添加
        // ...
    }
}

这样取消信号一来,既不再开新任务,已有的任务也在下次检查 isCancelled 时干净退出,已经下完的部分结果还能保留返回,而不是全盘丢弃。

Tip

处理”可以中途放弃、但已完成的成果要留住”的场景,addTaskUnlessCancelled + 返回可选结果 是标准套路。它体现了结构化并发”取消但不浪费已完成工作”的体贴。

1-9 性能直觉:并行不是白来的

并行能加速,但也不是”拆得越细越好”。每个子任务都有调度开销,如果一件事本身只要几毫秒,硬拆成几百个子任务,调度成本反而可能拖慢整体。

经验上,只有当单个子任务确实耗时(比如网络请求、大文件解析、复杂计算),并行才有明显收益。对一堆极轻的操作,async let/任务组带来的并行收益,可能被创建和协调任务的开销吃掉。所以”要不要并行”首先看任务够不够重,而不是看语法炫不炫。

Note

系统能同时跑多少个子任务,取决于机器的核心数和当前负载。任务组会自己权衡”同时跑几个”,你不必手动指定并发上限——这也是结构化并发省心的地方。

1-10 小结

并行化异步工作靠结构化并发:async let 适合数量已知的少量子任务,写完就并行、最后 await 一起收;withTaskGroup 适合数量运行期才定的批量子任务,能动态添加并逐个收集结果,抛错用 withThrowingTaskGroup。结构化并发保证父等子、父子同取消,把并发安全内置进语法。

下一章深入 Task 本身:任务树、取消机制,以及非结构化任务怎么用。