概述

任务组是一种并行执行多个任务的机制。在本文中,我们将从任务组的基本用法到包括取消的详细行为来了解。

文中的操作验证是使用 Xcode 14 Beta 5 完成的。

TL;博士

  • 当您想要并行运行子任务时使用任务组。特别适合执行动态数量的返回相同类型的子任务
  • 可以使用withTaskGroupwithThrowingTaskGroup 创建任务组。如果子任务没有抛出错误,则使用前者,如果有,则使用后者。
  • 作为一个基本流程,在传递给with(Throwing)TaskGroup的闭包中编写一个生成子任务的流程和一个接收它的流程@
    • 使用addTask(UnlessCancelled) 创建子任务。通常,for 语句用于翻转 Collection 的元素并基于它生成任务。
    • 接收处理通常用for try await ... in完成,但处理可以灵活地使用TaskGroupAsyncSequence这一事实来编写
  • 如果有子任务在离开进程时未明确await,则任务组将awaitasync let 的行为不同,因为它在退出范围时取消不是 await 的子任务
  • 当传递的闭包抛出错误或调用cancelAll 时,任务组取消所有剩余的子任务
    • 请注意,单个子任务抛出错误不会导致取消,只是该子任务不会返回结果。

何时使用任务组

首先,让我们看看任务组在哪里有用。考虑在 Todo 应用中显示未完成的 Todo。从 API 获取 Todo 的信息,如下所示

  • 获取未完成的待办事项 ID 列表的 API
  • 从 ID 获取 Todo 详细信息的 API

假设有两个

enum TodoAPI {
    static func getUndoneTodoIDs() async throws -> [String] { /* ... */ }
    static func getTodoDetail(id: String) async throws -> Todo { /* ... */ }
}

考虑到API的结构,需要先获取带有getUndoneTodoIDs的Todo ID列表,对响应中包含的每个ID调用getTodoDetail,汇总返回的Todo,我明白了。代码将如下所示:

func getUndoneTodos() async throws -> [Todo] {
    let todoIDs = try await TodoAPI.getUndoneTodoIDs()
        
    var todos: [Todo] = []
    for id in todoIDs {
        let todo = try await TodoAPI.getTodoDetail(id: id)
        todos.append(todo)
    }
    return todos
}

这段代码有一个明显的问题。由于getTodoDetail是串行调用的,第二个请求是在第一个请求完成后执行的,所以getUndoneTodoIDs返回的ID越多,完成的时间就越长。如果getTodoDetail的响应时间大约为1秒,getUndoneTodoIDs返回了10个ID,那么完成getUndoneTodos的整个执行需要10多秒。

为 ID 数量并行调用getTodoDetail 解决了这个问题,因为第二个请求不必等待第一个请求的结果。这就是任务组来救援的地方。我们将在本文后面看细节,但一般用法如下:

func getUndoneTodos() async throws -> [Todo] {
    let todoIDs = try await TodoAPI.getUndoneTodoIDs()
        
    // 1
    return try await withThrowingTaskGroup(of: Todo.self, returning: [Todo].self) { group in
        // 2
        for id in todoIDs {
            group.addTask {
                try await TodoAPI.getTodoDetail(id: id)
            }
        }
        
        // 3
        var todos: [Todo] = []
        for try await todo in group {
            todos.append(todo)
        }
        return todos
    }
}

首先,1 调用withThrowingTaskGroup 生成一个任务组。在第三个参数的闭包中写出你想要实际做的处理,但是TaskGroup的实例会被传递给闭包的参数。 2 使用 for 语句启动子任务。在一个不使用Task Groups的例子中,forawait在句子中做了,所以直到第一个Todo的获取完成后才能开始获取第二个Todo。,addTask的@ 987654353@ 不会阻止仅创建任务,因此您可以开始并行获取所有待办事项而无需等待结果。最后可以得到for try await ... inTaskGroup3返回的结果。

通过如上并行获取Todo,当getTodoDetail的响应在1秒内完成,在最成功的情况下,不管有多少个ID,都可以在1秒左右检索到所有的ID。Todo的获取完成。
需要注意的是,传递给for try await ... in 的 Todo 的顺序将是完成的顺序,而不是子任务的创建顺序。所以todoIDsTodogetUndoneTodos的返回值中的顺序一般是不同的。如果要对齐这些的顺序,需要自己写代码来保证。
此外,任务组没有并行执行限制。举个极端的例子,如果getUndoneTodoIDs返回1000个id,1000个getTodoDetail会并行运行。如果你想将并行执行的数量保持在一定水平以下,你需要自己编写流程。

作为Task Groups的一个使用场景,比如这个例子

  • 我想并行执行运行时确定的进程数
  • 每个操作都有相同的结果类型

这是典型的1.在这种情况下,使用for 语句在addTask 开始您想要并行执行的处理,并在for try await ... in 接收结果。

反之,如果你要并行执行的进程数量是在编译时确定的,或者这些进程的结果类型不同,你可以使用async let,这是Swift Concurrency中另一种创建子任务的方法。有很多.我不会在本文中讨论它,但是例如,如果您可以将图像附加到 Todo 并且您需要从不同的 API 端点获取 Todo 和图像,您可以像这样使用async let:这使您可以并行获取 Todo 和图像。

enum TodoAPI {
    static func getTodoDetail(id: String) async throws -> Todo { /* ... */ }
    static func getTodoImage(id: String) async throws -> UIImage? { /* ... */ }
}

func getTodoWithImage(id: String) async throws -> (Todo, UIImage?) {
    async let todo = TodoAPI.getTodoDetail(id: id)
    async let image = TodoAPI.getTodoImage(id: id)
    return try await (todo, image)
}

使用任务组

现在您对任务组有了一个概述,让我们更仔细地看看如何使用它们。

两种生成方法

在上一节中,我们使用withThrowingTaskGroup 创建任务组,但还有另一个函数withTaskGroup。让我们看看每个签名。

func withTaskGroup<ChildTaskResult, GroupResult>(
    of childTaskResultType: ChildTaskResult.Type,
    returning returnType: GroupResult.Type = GroupResult.self,
    body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult where ChildTaskResult : Sendable

func withThrowingTaskGroup<ChildTaskResult, GroupResult>(
    of childTaskResultType: ChildTaskResult.Type,
    returning returnType: GroupResult.Type = GroupResult.self,
    body: (inout ThrowingTaskGroup<ChildTaskResult, Error>) async throws -> GroupResult
) async rethrows -> GroupResult where ChildTaskResult : Sendable

这两个函数的区别在于withTaskGroup不会抛出错误,而withThrowingTaskGroup写主处理body变成throws整个函数变成rethrows我所在的地方。这似乎有点复杂,但我认为如果您要并行运行的子任务抛出错误,请记住使用withThrowingTaskGroup,如果没有,则使用withTaskGroup

作为如何使用withTaskGroup 的示例,考虑一个并行调用不抛出错误的函数geenerateRandomNumber 以获得随机数序列的示例。

func generateRandomNumber() async -> Int { /* ... */ }

func generateRandomNumbers(count: Int) async -> [Int] {
    await withTaskGroup(of: Int.self, returning: [Int].self) { group in
        for _ in 0..<count {
            group.addTask {
                await generateRandomNumber()
            }
        }
        
        var randomNumbers: [Int] = []
        for await number in group {
            randomNumbers.append(number)
        }
        return randomNumbers
    }
}

它与withThrowingTaskGroup的不同之处在于,在汇总结果时使用for await ... in而不是for try await ... in,并且try不需要调用withTaskGroup本身,但另一方面,其他部分相同。是。 Swift Concurrency 使用errors来实现取消机制,所以我觉得withThrowingTaskGroup在写生产代码的时候用得比较多。

下面在谈withTaskGroupwithThrowingTaskGroup的共同问题时,将两者统称为with(Throwing)TaskGroup

让我们看一下with(Throwing)TaskGroup 参数。它需要三个参数,第三个body 是一个闭包,它接收(Throwing)TaskGroup 以创建子任务并处理结果。将子任务的结果类型传递给第一个of,将body的返回类型,即with(Throwing)TaskGroup本身的返回类型传递给第二个returning。在上一节 Todo 应用的例子中,子任务是Todobody 返回ArrayTodo,所以of 返回Todo.selfreturning [Todo].self 正在交接returning 可以是 of Collection 这样的,但情况并非总是如此。例如,如果您想要一个用逗号分隔的未完成的待办事项标题字符串,请为returning 指定String.self,如下所示。

func getUndoneTodosTitleString() async throws -> String {
    let todoIDs = try await TodoAPI.getUndoneTodoIDs()
        
    return try await withThrowingTaskGroup(of: Todo.self, returning: String.self) { group in
        for id in todoIDs {
            group.addTask {
                try await TodoAPI.getTodoDetail(id: id)
            }
        }
        
        var todoTitles: [String] = []
        for try await todo in group {
            todoTitles.append(todo.title)
        }
        return todoTitles.joined(separator: ",")
    }
}

至此,我们已经显式编写了returning,但是从签名中可以看出,returning 有一个默认参数,与bodywith(Throwing)TaskGroup 的返回值类型相同。如果类型推断效果很好,则无需编写它,因此基本上您自己显式编写它的情况较少。

func getUndoneTodosTitleString() async throws -> String {
    let todoIDs = try await TodoAPI.getUndoneTodoIDs()
        
    // ✅ returning を省略してもコンパイルが通る
    return try await withThrowingTaskGroup(of: Todo.self) { group in
		// ...
    }
}

接下来,我们仔细看看body的写法,这是传递给with(Throwing)TaskGroup的主要过程。与 Todo 应用示例的情况一样,body 的处理基本上是

  1. 衍生子任务以并行运行
  2. 接收和处理子任务结果

    由于分为两个阶段,我们将分别组织。

    生成子任务

    子任务由TaskGroup 实例的addTask 方法创建。作为Task Groups的一种用法,有很多情况是为某个Collection的每个元素生成子任务,所以addTask经常像Todo应用示例一样在for语句中完成。我猜。当然,您可以在 for 语句之外使用 addTask,如下所示:

    func getTwoTodos() async throws -> [Todo] {
        try await withThrowingTaskGroup(of: Todo.self, returning: [Todo].self) { group in
            group.addTask {
                try await TodoAPI.getTodoDetail(id: "id1")
            }
            group.addTask {
                try await TodoAPI.getTodoDetail(id: "id2")
            }
            
            var todos: [Todo] = []
            for try await todo in group {
                todos.append(todo)
            }
            return todos
        }
    }
    

    到目前为止我省略了它,但是如果您查看addTask 的签名,您可以看到priority 可以与Task.initTask.detached 以相同的方式传递。

    mutating func addTask(
        priority: TaskPriority? = nil,
        operation: @escaping () async -> ChildTaskResult
    )
    

    显式传递 priority 如下所示:

    // ...
    group.addTask(priority: .background) {
        try await TodoAPI.getTodoDetail(id: "id1")
    }
    // ...
    

    如果省略priorityTask中的priority会使用生成任务组的,所以我认为除非有特殊情况,否则应该省略。

    addTaskUnlessCancelled 是一种类似于addTask 的方法。

    mutating func addTaskUnlessCancelled(
        priority: TaskPriority? = nil,
        operation: @escaping () async -> ChildTaskResult
    ) -> Bool
    

    addTask 的区别是

    • 如果任务组已取消,则不要生成子任务
    • 返回Bool 是否创建子任务

    关于它。如果您不希望子任务在被取消后甚至开始处理,您会使用addTaskUnlessCancelled,但我认为这样的情况并不多。如果子任务实现了正确处理取消,即使子任务在任务组已经取消的情况下以addTask启动,它也会立即响应取消并退出进程,这是没用的,因为它没有' t 使用任何资源。但是,如果子任务不支持取消或者想一直添加子任务直到任务组被取消,可以使用addTaskUnlessCancelled,这样就可以记住它的存在了。

    接收子任务结果

    在 Todo 应用示例中,for try await ... in 收到了addTask 启动的子任务的结果,但这是可能的,因为TaskGroup 符合AsyncSequence。有关如何使用 AsyncSequence 的更多信息,请参阅文档。

    使用TaskGroupAsyncSequence 的事实,您还可以收到for try await ... in 以外的结果。比如Todo应用的例子中,子任务返回的Todoappend到预先创建的Array,但是如果使用AsyncSequencereduce方法,也是一样的可以的。你可以写的更简洁。

    func getUndoneTodos() async throws -> [Todo] {
        let todoIDs = try await TodoAPI.getUndoneTodoIDs()
            
        return try await withThrowingTaskGroup(of: Todo.self, returning: [Todo].self) { group in
            for id in todoIDs {
                group.addTask {
                    try await TodoAPI.getTodoDetail(id: id)
                }
            }
    
    -       var todos: [Todo] = []
    -       for try await todo in group {
    -           todos.append(todo)
    -       }
    -       return todos
    +       return try await group.reduce(into: []) { result, todo in result.append(todo) }
        }
    }
    

    这里我就不多举例了,不过AsyncSequence还有其他的方法,比如mapfilterTaskGroup实例,可以让流程简单易懂。。

    还有,TaskGroup有一个next的方法,可以用和AsyncIteratorProtocol一样的图片一个一个的提取值。例如,如下写法与for try await ... in 含义相同。

    func getUndoneTodos() async throws -> [Todo] {
        let todoIDs = try await TodoAPI.getUndoneTodoIDs()
            
        return try await withThrowingTaskGroup(of: Todo.self, returning: [Todo].self) { group in
            for id in todoIDs {
                group.addTask {
                    try await TodoAPI.getTodoDetail(id: id)
                }
            }
    
            var todos: [Todo] = []
    -       for try await todo in group {
    -           todos.append(todo)
    -       }
    +       while let todo = try await group.next() {
    +           todos.append(todo)
    +       }
            return todos
    
        }
    }
    

    仅凭这一点,就只能使用for try await ... in,所以感觉不到优点,但是比如要启动几个子任务,使用返回最快的结果,可以写成如下增加。由于next 只读取一次,与for try await ... in 不同,无需等待所有子任务即可返回结果。

    func getFirstNumber() async throws -> Int {
        try await withThrowingTaskGroup(of: Int.self) { group in
            for _ in 0..<10 {
                group.addTask {
                    try await getNumber()
                }
            }
            
            let number = try await group.next()!
            group.cancelAll()
            return number
        }
    }
    

    派生自nextnextResult 也很有用。 nextfor try await ... in 在子任务抛出错误时重新抛出错误,所以当子任务抛出错误时,所有其他子任务都根据 Swift 并发取消机制取消。 nextResult 收到 Swift 的 Result 类型的 failure 错误,而不是按原样抛出它,即使子任务抛出错误,所以当子任务失败时取消不起作用。
    您可以使用它来执行诸如抛出 N 个容易失败的异步操作并返回前 M 个成功值之类的事情。

    func getResultFromFailureProneAPI() async throws -> Int { /* ... */  }
    
    func getSomeResultsFromFailureProneAPI(n: Int, m: Int) async throws -> [Int] {
        await withThrowingTaskGroup(of: Int.self) { group in
            for _ in 0..<n {
                group.addTask {
                    try await getResultFromFailureProneAPI()
                }
            }
            
            var results: [Int] = []
            while let result = await group.nextResult() {
                switch result {
                case .success(let value):
                    results.append(value)
                    if results.count >= m {
                        group.cancelAll()
                        return results
                    }
                case .failure:
                    break
                }
            }
            return results
        }
    }
    

    我认为在任务组中接收结果通常使用for try await ... in 完成,但您可以根据您的要求灵活地进行操作,例如此处介绍的一些模式。

    任务组和子任务生命周期

    Swift 结构化并发的核心是“任务树中的子任务不能超过其父任务”的原则。这使得更容易了解并行处理的状态,并且还可以取消所有正在进行的并发处理。

    任务组在退出 body 之前隐含地遵守此原则 await 对于不在 body 内的 await 的子任务。这是因为如果您在没有 await 子任务的情况下退出 body,则子任务将比任务组本身中的任务生存时间更长。

    我们来看一个具体的例子。在下面的例子中,我们试图退出body,而不是等待addTask 的子任务for try await ... in

    func double(number: Int) async throws -> Int {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        let doubled = number * 2
        print("returning \(doubled)")
        return doubled
    }
    
    func execTaskGroup() async throws {
        await withThrowingTaskGroup(of: Int.self) { group in
            for n in 1...3 {
                group.addTask {
                    try await double(number: n)
                }
            }
    		print("finishing body")
        }
        print("finishing execTaskGroup")
    }
    

    运行 execTaskGroup 会得到以下输出:

    finishing body
    -- ここで1秒経過 --
    returning 6
    returning 2
    returning 4
    finishing execTaskGroup
    

    body的最后一条print语句执行完之后,double虽然没有显式地执行完await,但执行到最后,然后execTaskGroup就完成了。明白了。这表明任务组在完成自己之前等待不是await 的子任务。

    这可能看起来很明显,但实际上,async let,另一种在 Swift Concurrency 中创建子任务的方式,以不同的策略实现了“子任务不能比父任务寿命长”的原则。该策略是在退出范围之前隐式取消不是await 的子任务。
    我还将看到async let 的实际行为。下面的代码也以与任务组相同的方式调用了三次double,并试图在没有await 的情况下退出范围。

    func execAsyncLets() async throws {
        async let n1 = double(number: 1)
        async let n2 = double(number: 2)
        async let n3 = double(number: 3)
    
        print("finishing execAsyncLets")
    }
    

    运行 execAsyncLets 给出以下输出:

    finishing execAsyncLets
    

    与任务组不同,returning [n] 不会打印。这是因为在退出execAsyncLets 的范围时,取消了对double 的调用,而不是取消了await。例如,您可以看到下面的print 调试实际上正在发生取消。

    func double(number: Int) async throws -> Int {
    +   do {
            try await Task.sleep(nanoseconds: 1_000_000_000)
    +   } catch {
    +       if Task.isCancelled {
    +           print("double cancelled with error: \(error)")
    +           throw error
    +       }
    +   }
        let doubled = number * 2
        print("returning \(doubled)")
        return doubled
    }
    

    如果在上面添加print 并再次运行execAsyncLets,输出将改变如下。

    finishing execAsyncLets
    double cancelled with error: CancellationError()
    double cancelled with error: CancellationError()
    double cancelled with error: CancellationError()
    

    可以看到退出execAsyncLets的作用域时,三个子任务都被取消了。

    如上,

    • async let 在退出范围时取消不是await 的子任务
    • 任务组awaitawait退出范围时的子任务

    我认为您应该意识到存在差异。由于这种差异,任务组可以更轻松地处理不返回值的集合。比如你想写Array的元素如下图,自然要返回addTask的进程,因为每个子任务都没有返回值。通过隐式await所有子任务不写任何东西,保证在所有save完成后退出作用域。

    func saveEntities(entities: [Entity]) async throws {
        await withTaskGroup(of: Void.self) { group in
            for entity in entities {
                group.addTask {
                    await Database.save(entity)
                }
            }
            // ✅ 何も書かなくてもすべての save が完了するのを待ってくれる
        }
    }
    

    在这种情况下,如果你想在所有子任务完成后做某事,你也可以使用waitForAll

    func saveEntities(entities: [Entity]) async throws {
        await withTaskGroup(of: Void.self) { group in
            for entity in entities {
                group.addTask {
                    await Database.save(entity)
                }
            }
    +       await group.waitForAll()
    +       print("finished all tasks")
        }
    }
    

    for await _ in group {} 做同样的事情,但使用waitForAll 更清晰、更简洁。

    取消任务组

    到目前为止,本文中的示例并未真正涉及子任务失败或创建任务组的父任务被取消的可能性。现在让我们谈谈取消任务组。

    在以下三种情况下取​​消任务组。

    • 如果bodywithThrowingTaskGroup 抛出错误
    • 如果创建任务组的任务被取消
    • cancelAll 被调用时

    如果bodywithThrowingTaskGroup 抛出错误

    在结构化并发中,如果在任务树的任何地方抛出错误,此错误将传播,直到树中的所有任务都被取消。同样对于任务组,如果在withThrowingTaskGroupbody 中抛出错误,则任务组的所有未完成的子任务都将被取消。

    以下面的代码为例。

    • 1 秒后返回 1
    • 2 秒后抛出 CancellationError
    • 3 秒后返回 3

    创建了三个子任务,并在for try await ... in 等待结果。

    func printNumbers() async throws {
        do {
            let result = try await withThrowingTaskGroup(of: Int.self) { group in
                group.addTask {
                    try await Task.sleep(nanoseconds: 1_000_000_000)
                    print("returning 1")
                    return 1
                }
                
                group.addTask {
                    try await Task.sleep(nanoseconds: 2_000_000_000)
                    print("throwing error")
                    throw CancellationError()
                }
                
                group.addTask {
                    try await Task.sleep(nanoseconds: 3_000_000_000)
                    print("returning 3")
                    return 3
                }
                
                var result: [Int] = []
                for try await value in group {
                    result.append(value)
                }
                return result
            }
            print("task group completed: \(result)")
        } catch {
            print("task group cancelled")
        }
    }
    

    当我运行它时,我得到以下输出:

    returning 1
    throwing error
    task group cancelled
    

    返回1 后,将引发错误并取消任务组。可以看到returning 3没有打印出来,因为Task Group的所有子任务也都被取消了。

    请注意,只有在body 中引发错误时才会发生取消。取消特定子任务不会影响整个任务组,它根本不会返回结果。上面例子中,Task Group被取消是因为子任务抛出的错误在body的正下方for try await ... in再次抛出,而子任务本身抛出的错误被取消了,说明它没有导致至为了确认这一点,使用上一节介绍的nextResult来处理子任务抛出的错误并等待结果。

    func printNumbers() async throws {
        do {
            let result = try await withThrowingTaskGroup(of: Int.self) { group in
                group.addTask {
                    try await Task.sleep(nanoseconds: 1_000_000_000)
                    print("returning 1")
                    return 1
                }
                
                group.addTask {
                    try await Task.sleep(nanoseconds: 2_000_000_000)
                    print("throwing error")
                    throw CancellationError()
                }
                
                group.addTask {
                    try await Task.sleep(nanoseconds: 3_000_000_000)
                    print("returning 3")
                    return 3
                }
                
                var result: [Int] = []
    -           for try await value in group {
    -               result.append(value)
    -           }
    +           while let nextResult = await group.nextResult() {
    +               switch nextResult {
    +               case .success(let value):
    +                   result.append(value)
    +               case .failure(let error):
    +                   print("ignoring error: \(error)")
    +               }
    +           }
                return result
            }
            print("task group completed: \(result)")
        } catch {
            print("task group cancelled")
        }
    }
    

    nextResult 导致 switch 并忽略子任务引发的错误。当我运行它时,我得到以下输出:

    returning 1
    throwing error
    ignoring error: CancellationError()
    returning 3
    task group completed: [1, 3]
    

    抛出错误的子任务被忽略,任务组继续执行,最终返回忽略错误[1, 3]的所有子任务的结果。由于在某些情况下您可以像这样灵活地处理错误,因此抛出错误的子任务不会直接导致取消是很重要的。

    如果创建任务组的任务被取消

    由于结构化并发机制,如果创建任务组的任务被取消,任务组也将被取消。

    例如,使用上一节中使用的printNumbers,没有错误抛出过程。

    func printNumbers() async throws {
        do {
            let result = try await withThrowingTaskGroup(of: Int.self) { group in
                group.addTask {
                    try await Task.sleep(nanoseconds: 1_000_000_000)
                    print("returning 1")
                    return 1
                }
                
                group.addTask {
                    try await Task.sleep(nanoseconds: 2_000_000_000)
                    print("returning 2")
                    return 2
                }
                
                group.addTask {
                    try await Task.sleep(nanoseconds: 3_000_000_000)
                    print("returning 3")
                    return 3
                }
                
                var result: [Int] = []
                for try await value in group {
                    result.append(value)
                }
                return result
            }
            print("task group completed: \(result)")
        } catch {
            print("task group cancelled")
        }
    }
    

    让我们看看如果我们在它开始运行 2.5 秒后取消这个 printNumbers 会发生什么。

    func f() async throws {
        let targetTask = Task {
            try await printNumbers()
        }
        
        let cancellingTask = Task {
            try await Task.sleep(nanoseconds: 2_500_000_000)
            targetTask.cancel()
        }
        
        try await (targetTask.value, cancellingTask.value)
    }
    

    我得到的输出是:

    returning 1
    returning 2
    task group cancelled
    

    可以看到 Task Group 也被取消了,因为创建 Task Group 的任务在 Task Group 的第三个子任务返回 3 之前就被取消了。

    相关的,我也会整理一下withTaskGroup和取消的关系。与withThrowingTaskGroup 不同,withTaskGroup 不能用body 抛出错误,所以乍一看似乎无法取消。当然,你不能抛出错误,所以你不能自己是取消的起源,但是当任务树上的其他任务抛出错误时,你可以响应它结束执行,或者返回部分结果你可以通过返回来响应。例如,将子任务签名更改为不在 printNumbers 处引发错误:

    func printNumbersWithoutThrowing() async throws {
        let result = await withTaskGroup(of: Int?.self) { group in
            group.addTask {
                do {
                    try await Task.sleep(nanoseconds: 1_000_000_000)
                    print("returning 1")
                    return 1
                } catch {
                    print("receiving error, returning nil")
                    return nil
                }
            }
            
            group.addTask {
                do {
                    try await Task.sleep(nanoseconds: 2_000_000_000)
                    print("returning 2")
                    return 2
                } catch {
                    print("receiving error, returning nil")
                    return nil
                }
            }
            
            group.addTask {
                do {
                    try await Task.sleep(nanoseconds: 3_000_000_000)
                    print("returning 3")
                    return 3
                } catch {
                    print("receiving error, returning nil")
                    return nil
                }
            }
            
            var result: [Int?] = []
            for await value in group {
                result.append(value)
            }
            return result
        }
        print("task group completed: \(result)")
    }
    

    通过catch并在添加addTask的子任务中返回nil,子任务本身不会抛出错误。这允许我们使用withTaskGroup 而不是withThrowingTaskGroup 并删除相关的try

    让我们看看如果我们在 2.5 秒后取消这个 printNumbersWithoutThrowing 会发生什么。

    func f() async throws {
        let targetTask = Task {
            try await printNumbersWithoutThrowing()
        }
        
        let cancellingTask = Task {
            try await Task.sleep(nanoseconds: 2_500_000_000)
            targetTask.cancel()
        }
        
        try await (targetTask.value, cancellingTask.value)
    }
    

    执行结果如下。

    returning 1
    returning 2
    receiving error, returning nil
    task group completed: [Optional(1), Optional(2), nil]
    

    只有任务组的第三个子任务在收到取消的时候没有完成,但是它立即返回nil而不忽略取消。因此,printNumbersWithoutThrowing 不会同时获得所有子任务的完整结果,但它会在收到取消时尽其所能。

    如上所述,即使您创建了一个不会使用withTaskGroup 引发错误的子任务,也可以通过不导致取消本身来正确处理从父任务传播的取消。但是,可以考虑忽略取消并继续执行的子任务,因此取消时的行为取决于子任务的实现。

    cancelAll 被调用时

    当您想从任务组中取消时,即使子任务中没有发生错误,也可以调用TaskGroup 中的cancelAll 方法。

    cancelAll 有用的一个示例是在设置超时时进行异步处理。

    func getNumber() async throws -> Int { /* ... */ }
    
    func getNumberWithTimeout(timeoutNanoseconds: UInt64) async throws -> Int {
        try await withThrowingTaskGroup(of: Int.self) { group in
            group.addTask {
                try await getNumber()
            }
            
            group.addTask {
                try await Task.sleep(nanoseconds: timeoutNanoseconds)
                throw CancellationError()
            }
            
            let result = try await group.next()!
            group.cancelAll()
            return result
        }
    }
    

    一个任务组创建两个子任务。一个是getNumber,就是你要执行的进程,另一个是超时任务。超时任务等待指定时间然后抛出CancellationError,这样当getNumber的时间出乎意料的长时,可以取消进程,而无需让getNumberWithTimeout的调用者等待。

    如果getNumber 在超时前成功完成,try await group.next()! 会给你结果。请注意,在此之后,group.cancelAll() 立即取消未完成的子任务。这种情况下未完成的子任务就是超时任务。

    考虑一下如果没有cancelAll 调用会发生什么。如前所述,任务组在退出 body 时隐式地 await 任何不是 await 的子任务。除非使用cancelAll 取消它,否则超时的子任务仍然没有await。将需要10 秒。 cancelAll 用于避免这种情况。

    参考

    1. 当然,这通常是任务组有用的地方,并且在某些情况下,您可能希望将任务组用于固定数量的操作,或者用于返回不同类型的操作。


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308623860.html

相关文章:

  • 2021-12-19
  • 2021-12-07
  • 2021-07-31
  • 2021-04-28
  • 2021-12-28
  • 2021-11-08
  • 2021-11-05
猜你喜欢
  • 2021-07-19
  • 2021-12-04
  • 2022-02-16
  • 2022-12-23
  • 2021-08-07
  • 2021-05-30
相关资源
相似解决方案