Swift并发模型中的TaskGroup和async let
Swift并发模型中的TaskGroup和async let

Swift并发模型中的TaskGroup和async let

TaskGroup 和 async let 都是用于并发并行执行多个异步任务的工具,但它们各自适用于不同的场景。

async let:并发启动多个已知数量的异步任务,适合数量固定、结构清晰的并发。

TaskGroup:动态创建多个异步任务并等待全部完成,适合数量不定、动态生成的任务。

async let

并发执行多个已知任务。

async let image1 = loadImage(from: "url1")
async let image2 = loadImage(from: "url2")

let result = try await [image1, image2]

另一种写法:

async let image1 = loadImage(from: "url1")
async let image2 = loadImage(from: "url2")

let result1 = try await image1
let result2 = try await image2

启动三个异步任务并发执行,而不是顺序。

最终使用 await 等待所有任务结果。

适合任务数量确定且类型一致(如并发加载多个图片)。

注意:不能用for循环(数量必须提前写死。)

TaskGroup

TaskGroup 是 Swift 结构化并发模型中的一种并发原语,用于管理“一组可以并发执行的异步子任务”。

func loadNumbers() async -> [Int] {
    await withTaskGroup(of: Int.self) { group in
        // 提交 2 个并发任务
        group.addTask { 1 }
        group.addTask { 2 }

        var results: [Int] = []

        // 逐个“按完成顺序”接收结果
        for await value in group {
            results.append(value)
        }

        return results
    }
}

// 调用
Task {
    let values = await loadNumbers()
    print(values)
}

1、withThrowingTaskGroup(for: Int.self):创建一个任务组(所有任务一起并发执行,支持抛错),指定每个子任务返回Int。

因为withThrowingTaskGroup是一个泛型函数,TaskGroup必须知道“每个子任务返回的类型”,因此需要指定类型,否则编译器会因为无法推断类型而报错。

2、group.addTask { … }:向当前任务组提交一个新的并发子任务。

当调用addTask时:

group.addTask { 1 }

立即创建一个并发子任务,子任务开始执行,当前协程不会等待任务完成,子任务的返回值通过 for await 或 next() 取出。

因此,当调用 addTask 时,提交任务并立即启动,不阻塞当前代码。

3、for await value in group:等待并处理每个子任务返回的值,当某个子任务完成,就会产生一个值并进入迭代,当全部子任务完成后,循环结束。

for-await循环会等待全部子任务完成后,再调用return返回。

伪代码逻辑

进入 TaskGroup 组
提交并发子任务 1(子任务开始执行)
提交并发子任务 2(子任务开始执行)

循环:
    等待并处理子任务
    添加返回值
循环结束:所有子任务完成

最后,返回数组

1、Task Group的next()方法

next() 方法用来“手动拉取下一个已完成子任务的结果”。

调用 next() 会挂起当前任务,直到某个子任务“先完成”为止。

当子任务完成后,返回子任务的结果。当组内所有任务都完成时,返回 nil。

while let value = await group.next() {
    results.append(value)
}

等价于:

for await value in group {
    results.append(value)
}

可以把for-in循环理解为语法糖,本质等价于持续调用 await group.next(),直到返回 nil。

2、失败场景

如果允许失败的TaskGroup(返回Optional),则可以使用:

await withTaskGroup(of: String?.self) { group in
    for i in 1...3 {
        group.addTask {
            await loadMaybe(id: i)
        }
    }
}

func loadMaybe(id: Int) async -> String? {
    if id == 2 { return nil }     // 模拟失败
    return "OK \(id)"
}

当任务失败时,返回nil,外层过滤nil。

3、错误触发并取消其余任务

enum FetchError: Error { case failed }

func riskyTask(_ id: Int) async throws -> Int {
    if id == 2 { throw FetchError.failed }
    return id * 10
}

func runThrowingGroup() async throws -> [Int] {
    try await withThrowingTaskGroup(of: Int.self) { group in
        for i in 1...3 {
            group.addTask {
                try await riskyTask(i)
            }
        }

        var results: [Int] = []

        do {
            for try await value in group {
                results.append(value)
            }
        } catch {
            group.cancelAll()   // 失败后取消剩余任务
            throw error
        }

        return results
    }
}

当任务发生报错后,使用cancelAll()取消剩余任务。

async let 和 TaskGroup的区别

async let 和 TaskGroup 都能实现并发执行多个任务,但是在设计目的、能力范围和使用场景上有明显区别。

1、不固定数量任务:async let不支持且不能循环,TaskGroup支持并且可以循环。

// async let 不支持动态循环
for url in urls {
    async let image = downloadImage(from: url) // 错误 
}

// TaskGroup 支持
for url in urls {
    group.addTask {
        try await downloadImage(from: url)
    }
}

2、任务控制能力:async let不支持添加任务时做逻辑判断,不支持添加任务时动态设置参数,不支持在子任务里处理错误,TaskGroup具有任务控制能力,可以添加任务并判断、记录和处理异常。

3、错误处理:async let支持错误传播,但不能捕获局部错误。当整个await抛出错误时,不会处理其他任务。TaskGroup支持错误传播和捕获局部错误。还可以处理每个任务抛出的错误,灵活控制。

// async let:某个任务失败,整体 throw,没法控制哪个失败
async let a = try mayFail1()
async let b = try mayFail2()
let result = try await [a, b]

// TaskGroup:每个任务都可单独 try-catch
group.addTask {
    do {
        return try await mayFail()
    } catch {
        print("一个任务失败了,但继续")
        return defaultValue
    }
}

4、性能优化:async let在声明后立即开始,TaskGroup在addTask时立即开始。

async let a = fetchData1()  // 执行并启动任务
async let b = fetchData2()  // 执行并启动任务
async let c = fetchData3()  // 执行并启动任务

上面这三行代码,每一行一执行就启动任务,而不是等到 await 的时候才开始执行。

三个任务是并发执行的,只是稍后再取结果。

try await withThrowingTaskGroup(of: Int.self) { group in
    group.addTask { try await fetchData1() }
    group.addTask { try await fetchData2() }
    group.addTask { try await fetchData3() }
}

这里任务在调用group.addTask { … } 的时候就立刻开始执行了。

两者在任务开始时机上是一样的,都是定义时立刻启动。

区别主要是:

async let 是语法上的声明。

TaskGroup 可以在任意位置调用 addTask,甚至放进 for 循环里动态控制。

5、返回结构顺序:async let按声明顺序固定,TaskGroup按任务完成顺序,如果需要保留顺序,需要自己记录index。

async let a = fetch1()
async let b = fetch2()
async let c = fetch3()

let result1 = try await a  // 对应 fetch1()
let result2 = try await b  // 对应 fetch2()
let result3 = try await c  // 对应 fetch3()

顺序始终一致,结果也一一对应。

try await withThrowingTaskGroup(of: String.self) { group in
    group.addTask { try await fetch("A") }
    group.addTask { try await fetch("B") }
    group.addTask { try await fetch("C") }

    var results: [String] = []

    for try await result in group {
        results.append(result)
    }

    return results
}

这里的 result 是哪个任务先完成就先返回,不是 addTask 的顺序。

比如写了 A→B→C,结果可能是 B 最快返回,顺序就变成:

["B 的结果", "A 的结果", "C 的结果"]

如果想要在TaskGroup中保证顺序,可以在addTask里手动标记顺序:

group.addTask {
    let result = try await fetch("A")
    return (index: 0, value: result)
}

然后最后排序:

results.sort { $0.index < $1.index }

注意事项

async let 不能放在循环或条件语句中(因为数量必须在编译时固定):

// 错误:不能在 for 中用 async let
for url in urls {
    async let img = loadImage(from: url)
}

TaskGroup 不返回顺序结果,结果的顺序由任务完成顺序决定,如果需要有序结果,需额外处理。

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注