Swift 结构化并发 6 条铁律 —— 一张图 + 一套模板,让 `async let` / `TaskGroup` / `Task {}` 不再踩坑

思维导图(先保存,再阅读)

vbnet 复制代码
Swift Concurrency
├─ Structured(结构化并发,有父有子,有纪律)
│  ├─ async let        → 静态并发
│  └─ TaskGroup        → 动态并发
│  三大规则 EGG
│   ├─ E  Error 传播:出作用域即 取消+等待
│   ├─ G  Group 完成:父必等子
│   └─ G  Group 取消:父取消→子取消
│  特征
│   ├─ 生命周期=作用域
│   ├─ 隐式 await(作用域结束)
│   └─ 继承优先级 & TaskLocal,**不继承 actor**
└─ Unstructured(无父,野生)
   ├─ Task { }           → 继承上下文
   ├─ Task.detached { }  → 啥也不继承
   └─ 无 EGG 规则,一切靠自己

为什么分"结构化"与"非结构化"

维度 Structured Unstructured
能否成为子任务 ❌(只能是根)
能否成为父任务 ✅(再开 structured 子任务)
生命周期 绑定作用域 绑定引用
规则 EGG 自动生效 全无效
典型 API async let / TaskGroup Task {} / Task.detached {}

Structured 任务两大形态

async let ------ 静态并发

swift 复制代码
func fetchData() async {
    async let first  = fetchPart1()   // 立即启动子任务
    async let second = fetchPart2()
    async let third  = fetchPart3()

    let result = await (first, second, third) // 隐式等全部完成
    print(result)
}
// 离开作用域前,**所有子任务必须完成**,否则父任务不会完成

注意:

  • 顺序:await 不影响并发,三个子任务同时飞。
  • 只要作用域还在,就保证等;提前 return/throw 也会自动取消+等待其余子任务。

TaskGroup ------ 动态并发

swift 复制代码
func fetchData(count: Int) async {
    await withTaskGroup(of: String.self) { group in
        for i in 0..<count {
            group.addTask {          // 动态添加子任务
                await fetchPart(i)
            }
        }
        for await result in group {   // 谁跑完谁先处理
            print(result)
        }
    }   // 此处**隐式等待**所有子任务
}

优点:

  • 数量运行时决定
  • fail-fast:任一子任务抛错,其余子任务立即取消并传播第一个错误
  • 支持 group.cancelAll() 手动取消

Unstructured 任务:两座"根"

常规 Task { } ------ 继承上下文

swift 复制代码
enum TaskLocalStorage {
    @TaskLocal static var requestID: String?
}

Task {                                  // 继承
    print(Task.currentPriority)         // 继承调用方优先级
    print(TaskLocalStorage.requestID)   // 继承 TaskLocal
    // 若在外层 @MainActor,也继承 executor,但内部函数自己的 actor 隔离仍生效
}

Task.detached { } ------ 啥也不继承

swift 复制代码
Task.detached {
    // 优先级 = .medium(默认)
    // 无 TaskLocal
    // 一定跑在**全局并发线程池**,不会上主线程
}

使用场景:

  • 真正"后台孤岛"计算
  • 但99 % 场景用常规 Task { } + 函数自身 nonisolated 即可,detached 是最后 resort

三大铁律 EGG(只适用于 Structured)

E - Error 传播规则

定义:作用域因抛错而提前退出时,所有子任务自动取消 + 等待。

swift 复制代码
func fast() async throws { ... throw TestError() }
func slow() async throws { ... }

func parent() async throws {
    async let f = fast()   // 5 s 后抛错
    async let s = slow()   // 10 s 后完成
    try await (f, s)       // fast 先抛,slow 被**自动取消并等待**
}
// 离开作用域前,**所有子任务必须完成或被取消**,错误才继续向上抛

对比 Unstructured:

swift 复制代码
let root = Task {
    Task { try await fast() }   // 无父子关系
    Task { try await slow() }   // **不会被自动取消**
}
root.cancel()                   // 两个嵌套 Task 仍跑完

G - Group 完成规则

定义:父任务必须等所有子任务完成后才能完成。

swift 复制代码
let parent = Task {
    async let a = work()   // 10 s
    async let b = work()   // 10 s
    _ = await (a, b)
}
await parent.value        // **20 s 后才打印**
print("parent completes")

Unstructured 版本:

swift 复制代码
let root = Task {
    Task { await work() }  // 10 s
}
await root.value          // **立刻打印**,嵌套 Task 继续跑
print("root completes")   // 出现在 work 之前

G - Group 取消规则

定义:父任务被取消 → 所有子任务自动取消(协作式)。

swift 复制代码
let parent = Task {
    async let a = longWork()   // 10 s
    async let b = longWork()   // 10 s
    _ = await (a, b)
}
parent.cancel()              // a & b 收到 `CancellationError`
await parent.value

Unstructured 再次失效:

swift 复制代码
let root = Task {
    Task { await longWork() } // **收不到取消**
}
root.cancel()

上下文继承差异速查

继承项 Structured 子任务 Task {} Task.detached { }
优先级 ❌(默认 .medium)
TaskLocal
Actor 隔离 ❌(永远不继承,跑全局并发池) ✅(继承调用方 executor) ❌(全局池)

易错点:

async let/group.addTask 不会把代码钉在 @MainActor,即使外层是主线程;

内部函数自己的isolate决定最终 executor。

什么时候用哪种任务?一张表搞定

需求 选型
静态并发(固定数量) async let
动态并发 & fail-fast TaskGroup
同步→异步 Task { }
后台"孤岛"计算,不继承任何东西 Task.detached { }(最后 resort)
Fire-and-forget(不 await) Task { } 但记得手动管理生命周期

实战模板:网络层"结构化"封装

swift 复制代码
/// 并发下载多张图片,任一失败立即取消其余,返回首张
@MainActor
func fetchFirstImage(urls: [URL]) async throws -> UIImage {
    try await withThrowingTaskGroup(of: UIImage.self) { group in
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let image = UIImage(data: data) else {
                    throw URLError(.badURL)
                }
                return image
            }
        }
        // 只要第一张成功,其余自动取消
        for try await image in group {
            return image              // 提前返回 → 其余任务被取消
        }
        throw URLError(.badServerResponse)
    }
}

特点:

  • 结构化保证"失败即取消"
  • 离开作用域自动等待,不会泄漏任务

常见踩坑清单

踩坑 正确做法
async let 忘了 await 作用域结束自动等,但返回值别丢
Task { } 里开 async let 就当子任务 ❌ 无父子关系,EGG 规则全失效
想取消单个 async let 必须取消整个父任务;用 TaskGroup + cancelAll() 更细
Task.detached 当成"性能加速器" 99 % 用常规 Task { } + 函数自己的 nonisolated 即可

一句话总结

Structured = 有爹管:生命周期、取消、错误、完成,作用域帮你兜底。

Unstructured = 野生根:一切自己 await、自己 cancel、自己管理。

记住 EGG 只给"有爹"的任务吃,野生任务饿了自己煮。

相关推荐
HarderCoder2 小时前
深入理解 Swift Concurrency:从 async/await 到 Actor 与线程池的完整运行机制
swift
M-finder1 天前
Mac菜单栏综合工具FancyTool更新啦
mac·swift
HarderCoder3 天前
在同步代码里调用 async/await:Task 就是你的“任意门”
swift
HarderCoder3 天前
Swift 三目运算符指南:写法、场景与避坑
swift
YungFan3 天前
iOS26适配指南之UISlider
ios·swift
HarderCoder3 天前
一篇读懂 Swift 不透明类型:让带 associatedtype 的协议也能当返回值
swift
HarderCoder3 天前
`@dynamicCallable`:把 Swift 对象当函数喊
swift
HarderCoder4 天前
调试 Swift 并发:我到底在哪个 Actor?
swift
HarderCoder4 天前
`@preconcurrency` 完全导读:让旧代码平安驶上 Swift 并发快车道
swift