How to cancel a task group 如何取消任务组

How to cancel a task group 如何取消任务组

温馨提示:本文最后更新于2025-02-08 15:22:35,某些文章具有时效性,若有错误或已失效,请在下方留言

Swift’s task groups can be cancelled in one of three ways: if the parent task of the task group is cancelled, if you explicitly call cancelAll() on the group, or if one of your child tasks throws an uncaught error, all remaining tasks will be implicitly cancelled.
Swift 的任务组可以通过以下三种方式之一取消:如果任务组的父任务被取消,如果您在组上显式调用 cancelAll(),或者如果您的一个子任务抛出未捕获的错误,则所有剩余任务都将被隐式取消。

The first of those happens outside of the task group, but the other two are worth investigating.
其中第一个发生在任务组之外,但其他两个值得研究。

First, calling cancelAll() will cancel all remaining tasks. As with standalone tasks, cancelling a task group is cooperative: your child tasks can check for cancellation using Task.isCancelled or Task.checkCancellation(), but they can ignore cancellation entirely if they want.
首先,调用 cancelAll() 将取消所有剩余任务。与独立任务一样,取消任务组是协作的:您的子任务可以使用 Task.isCancelled 或 Task.checkCancellation() 检查取消,但如果需要,它们可以完全忽略取消。

I’ll show you a real-world example of cancelAll() in action in a moment, but before that I want to show you some toy examples so you can see how it works.
我稍后将向您展示一个实际的 cancelAll() 示例,但在此之前,我想向您展示一些玩具示例,以便您了解它是如何工作的。

We could write a simple printMessage() function like this one, creating three tasks inside a group in order to generate a string:
我们可以编写一个简单的 printMessage() 函数,例如这个函数,在一个组内创建三个任务以生成一个字符串:

func printMessage() async {
    let result = await withThrowingTaskGroup(of: String.self) { group in
        group.addTask { "Testing" }
        group.addTask { "Group" }
        group.addTask { "Cancellation" }

        group.cancelAll()
        var collected = [String]()

        do {
            for try await value in group {
                collected.append(value)
            }
        } catch {
            print(error.localizedDescription)
        }

        return collected.joined(separator: " ")
    }

    print(result)
}

await printMessage()
下载图标
how-to-cancel-a-task-group-1.zip
zip文件
49.7K

As you can see, that calls cancelAll() immediately after creating all three tasks, and yet when the code is run you’ll still see all three strings printed out.
如你所见,这会在创建所有三个任务后立即调用 cancelAll(),但是当代码运行时,你仍然会看到所有三个字符串都打印出来。

I’ve said it before, but it bears repeating and this time in bold: cancelling a task group is cooperative, so unless the tasks you add implicitly or explicitly check for cancellation calling cancelAll() by itself won’t do much.
我之前已经说过了,但值得重复一遍,这次是粗体:取消任务组是合作的,因此,除非您隐式或显式地检查取消任务,否则单独调用 cancelAll() 不会起到太大作用。

To see cancelAll() actually working, try replacing the first addTask() call with this:
要查看 cancelAll() 实际工作,请尝试将第一个 addTask() 调用替换为以下内容:

group.addTask {
    try Task.checkCancellation()
    return "Testing"
}

And now our behavior will be different: you might see “Cancellation” by itself, “Group” by itself, “Cancellation Group”, “Group Cancellation”, or nothing at all.
现在我们的行为将有所不同:您可能会看到 “Cancellation” 本身、“Group” 本身、“Cancellation Group”、“Group Cancellation”,或者什么都看不到。

To understand why, keep the following in mind:
要了解原因,请记住以下几点:

  1. Swift will start all three tasks immediately. They might all run in parallel; it depends on what the system thinks will work best at runtime.
    Swift 将立即启动所有 3 项任务。它们可能都并行运行;这取决于系统认为什么在运行时效果最好。
  2. Although we immediately call cancelAll(), some of the tasks might have started running.
    尽管我们立即调用 cancelAll(),但某些任务可能已经开始运行。
  3. All the tasks finish in completion order, so when we first loop over the group we might receive the result from any of the three tasks.
    所有任务都按完成顺序完成,因此当我们第一次循环遍历该组时,我们可能会收到这三个任务中任何一个的结果。

When you put those together, it’s entirely possible the first task to complete is the one that calls Task.checkCancellation(), which means our loop will exit, we’ll print an error message, and send back an empty string. Alternatively, one or both of the other tasks might run first, in which case we’ll get our other possible outputs.
当你把这些放在一起时,完全有可能要完成的第一个任务是调用 Task.checkCancellation() 的任务,这意味着我们的循环将退出,我们将打印一条错误消息,并发回一个空字符串。或者,其他一个或两个任务可能会先运行,在这种情况下,我们将获得其他可能的输出。

Remember, calling cancelAll() only cancels remaining tasks, meaning that it won’t undo work that has already completed. Even then the cancellation is cooperative, so you need to make sure the tasks you add to the group check for cancellation.
请记住,调用 cancelAll() 只会取消剩余的任务,这意味着它不会撤消已经完成的工作。即使这样,取消也是合作的,因此您需要确保添加到组中的任务检查是否取消。

With that toy example out of the way, here’s a more complex demonstration of cancelAll() that builds on an example from an earlier chapter. This code attempts to fetch, merge, and display using SwiftUI the contents of five news feeds.
有了那个玩具示例,下面是一个更复杂的 cancelAll() 演示,它基于前一章中的示例构建。此代码尝试使用 SwiftUI 获取、合并和显示五个新闻源的内容。

If any of the fetches throws an error the whole group will throw an error and end, but if a fetch somehow succeeds while ending up with an empty array it means our data quota has run out and we should stop trying any other feed fetches.
如果任何 fetch 抛出错误,整个组将抛出错误并结束,但如果 fetch 以某种方式成功,但以空数组结束,则意味着我们的数据配额已用完,我们应该停止尝试任何其他 feed fetch。

Here’s the code:  这是代码:

struct NewsStory: Identifiable, Decodable {
    let id: Int
    let title: String
    let strap: String
    let url: URL
}

struct ContentView: View {
    @State private var stories = [NewsStory]()

    var body: some View {
        NavigationStack {
            List(stories) { story in
                VStack(alignment: .leading) {
                    Text(story.title)
                        .font(.headline)

                    Text(story.strap)
                }
            }
            .navigationTitle("Latest News")
        }
        .task {
            await loadStories()
        }
    }

    func loadStories() async {
        do {
            try await withThrowingTaskGroup(of: [NewsStory].self) { group in
                for i in 1...5 {
                    group.addTask {
                        let url = URL(string: "https://hws.dev/news-\(i).json")!
                        let (data, _) = try await URLSession.shared.data(from: url)
                        try Task.checkCancellation()
                        return try JSONDecoder().decode([NewsStory].self, from: data)
                    }
                }

                for try await result in group {
                    if result.isEmpty {
                        group.cancelAll()
                    } else {
                        stories.append(contentsOf: result)
                    }
                }

                stories.sort { $0.id < $1.id }
            }
        } catch {
            print("Failed to load stories: \(error.localizedDescription)")
        }
    }
}
下载图标
how-to-cancel-a-task-group-2.zip
zip文件
24.8K

As you can see, that calls cancelAll() as soon as any feed sends back an empty array, thus aborting all remaining fetches. Inside the child tasks there is an explicit call to Task.checkCancellation(), but the data(from:) also runs check for cancellation to avoid doing unnecessary work.
如你所见,一旦任何 feed 发回一个空数组,它就会调用 cancelAll(),从而中止所有剩余的获取。在子任务中,有对 Task.checkCancellation()的显式调用,但 data(from:) 也会运行取消检查以避免执行不必要的工作。

The other way task groups get cancelled is if one of the tasks throws an uncaught error. We can write a simple test for this by creating two tasks inside a group, both of which sleep for a little time. The first task will sleep for 1 second then throw an example error, whereas the second will sleep for 2 seconds then print the value of Task.isCancelled.
任务组被取消的另一种方式是,如果其中一个任务引发未捕获的错误。我们可以通过在 group 中创建两个 task 来为此编写一个简单的测试,这两个 task 都休眠了一小段时间。第一个任务将休眠 1 秒,然后引发示例错误,而第二个任务将休眠 2 秒,然后打印 Task.isCancelled 的值。

Here’s how that looks:  这是它的样子:

enum ExampleError: Error {
    case badURL
}

func testCancellation() async {
    do {
        try await withThrowingTaskGroup(of: Void.self) { group in
            group.addTask {
                try await Task.sleep(for: .seconds(1))
                throw ExampleError.badURL
            }

            group.addTask {
                try await Task.sleep(for: .seconds(2 ))
                print("Task is cancelled: \(Task.isCancelled)")
            }

            try await group.next()
        }
    } catch {
        print("Error thrown: \(error.localizedDescription)")
    }
}

await testCancellation()
下载图标
how-to-cancel-a-task-group-3.zip
zip文件
49.7K

Note: Just throwing an error inside addTask() isn’t enough to cause other tasks in the group to be cancelled – this only happens when you access the value of the throwing task using next() or when looping over the child tasks. This is why the code sample above specifically waits for the result of a task, because doing so will cause ExampleError.badURL to be rethrown and cancel the other task.
注意: 仅在 addTask() 中抛出错误不足以导致组中的其他任务被取消——这只会发生在你使用 next() 访问抛出任务的值或循环子任务时。这就是为什么上面的代码示例专门等待任务的结果,因为这样做会导致 ExampleError.badURL 被重新抛出并取消另一个任务。

Calling addTask() on your group will unconditionally add a new task to the group, even if you have already cancelled the group. If you want to avoid adding tasks to a cancelled group, use the addTaskUnlessCancelled() method instead – it works identically except will do nothing if called on a cancelled group. Calling addTaskUnlessCancelled() returns a Boolean that will be true if the task was successfully added, or false if the task group was already cancelled.
在你的组上调用 addTask() 将无条件地向该组添加新任务,即使你已经取消了该组。如果您想避免将任务添加到已取消的组,请改用 addTaskUnlessCancelled() 方法 – 它的工作原理相同,只是在已取消的组上调用时不会执行任何作。调用 addTaskUnlessCancelled() 将返回一个布尔值,如果任务已成功添加,则该布尔值为 true,如果任务组已取消,则为 false。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享