2025-02-08 14:36:42
,某些文章具有时效性,若有错误或已失效,请在下方留言。Swift’s task groups are collections of tasks that work together to produce a single result. Each task inside the group must return the same kind of data, but if you use enum associated values you can make them send back different kinds of data – it’s a little clumsy, but it works.
Swift 的任务组是任务的集合,它们协同工作以产生单一结果。组中的每个任务都必须返回相同类型的数据,但是如果使用枚举关联值,则可以使它们发回不同类型的数据 – 这有点笨拙,但可以正常工作。
Creating a task group is done in a very precise way to avoid us creating problems for ourselves: rather than creating a TaskGroup
instance directly, we do so by calling the withTaskGroup(of:)
function and telling it the data type the task group will return. We give this function the code for our group to execute, and Swift will pass in the TaskGroup
that was created, which we can then use to add tasks to the group.
创建任务组的方式非常精确,以避免我们自己制造问题:我们不是直接创建 TaskGroup
实例,而是通过调用 withTaskGroup(of:)
函数并告诉它任务组将返回的数据类型来实现。我们向这个函数提供代码供我们的组执行,Swift 将传入创建的 TaskGroup
,然后我们可以使用它来向组添加任务。
First, I want to look at the simplest possible example of task groups, which is returning 5 constant strings, adding them into a single array, then joining that array into a string:
首先,我想看一下最简单的任务组示例,即返回 5 个常量字符串,将它们添加到单个数组中,然后将该数组联接到一个字符串中:
func printMessage() async {
let string = await withTaskGroup(of: String.self) { group in
group.addTask { "Hello" }
group.addTask { "From" }
group.addTask { "A" }
group.addTask { "Task" }
group.addTask { "Group" }
var collected = [String]()
for await value in group {
collected.append(value)
}
return collected.joined(separator: " ")
}
print(string)
}
await printMessage()
I know it’s trivial, but it demonstrates several important things:
我知道这很微不足道,但它展示了几个重要的事情:
- We must specify the exact type of data our task group will return, which in our case is
String.self
so that each child task can return a string.
我们必须指定任务组将返回的确切数据类型,在本例中为String.self
,以便每个子任务都可以返回一个字符串。 - We’re handed the new task group inside our closure, using
group in
.
我们在闭包中被交给了新的 task group,使用group in
。 - We call
addTask()
once for each task we want to add to the group, passing in the work we want that task to do.
对于要添加到组中的每个任务,我们都会调用addTask()
一次,并传入我们希望该任务执行的工作。 - Task groups conform to
AsyncSequence
, so we can read all the values from their children usingfor await
, or by callinggroup.next()
repeatedly.
任务组符合AsyncSequence
,因此我们可以使用for await
或通过重复调用group.next()
从其子项中读取所有值。 - Because the whole task group executes asynchronously, we must call it using
await
.
因为整个任务组是异步执行的,所以我们必须使用await
来调用它。
However, there’s one other thing you can’t see in that code sample, which is that our task results are sent back in completion order and not creation order.
但是,在该代码示例中,还有一件事是看不到的,那就是我们的任务结果是按完成顺序而不是创建顺序发送回的。
That is, our code above might send back “Hello From A Task Group”, but it also might send back “Task From A Hello Group”, “Group Task A Hello From”, or any other possible variation – the return value could be different every time.
也就是说,上面的代码可能会发回 “Hello From A Task Group”,但也可能会发回 “Task From A Hello Group”、“Group Task A Hello From” 或任何其他可能的变体 – 每次返回值都可能不同。
Note: From Swift 6.1 onwards, you can create withTaskGroup()
without using the of
parameter – Swift figures out what type to use based on the first child task you add to the group.
注意: 从 Swift 6.1 开始,您可以在不使用 of
参数的情况下创建 withTaskGroup()
– Swift 会根据您添加到组中的第一个子任务来确定要使用的类型。
Tasks created using withTaskGroup()
cannot throw errors. If you want them to be able to throw errors that bubble upwards – i.e., that are handled outside the task group – you should use withThrowingTaskGroup()
instead.
使用 withTaskGroup()
创建的任务不会引发错误。如果你希望它们能够抛出向上冒泡的错误——即在任务组之外处理的错误——你应该改用 withThrowingTaskGroup()。
To demonstrate this, and also to demonstrate a more real-world example of TaskGroup
in action, we could write some code that fetches several news feeds and combines them into one list:
为了演示这一点,也为了演示 TaskGroup
的更实际示例,我们可以编写一些代码来获取多个新闻源并将它们组合成一个列表:
struct NewsStory: Decodable, Identifiable {
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 {
stories = 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)
return try JSONDecoder().decode([NewsStory].self, from: data)
}
}
var allStories = [NewsStory]()
for try await stories in group {
allStories.append(contentsOf: stories)
}
return allStories.sorted { $0.id > $1.id }
}
} catch {
print("Failed to load stories")
}
}
}
In that code you can see we have a simple struct that contains one news story, a SwiftUI view showing all the news stories we fetched, plus a loadStories()
method that handles fetching and decoding several news feeds into a single array.
在该代码中,你可以看到我们有一个简单的结构体,其中包含一个新闻报道,一个显示我们获取的所有新闻报道的 SwiftUI 视图,以及一个 loadStories()
方法,该方法处理获取多个新闻提要并将其解码为单个数组。
There are four things in there that deserve special attention:
其中有四件事值得特别注意:
- Fetching and decoding news items might throw errors, and those errors are not handled inside the tasks, so we need to use
withThrowingTaskGroup()
to create the group.
获取和解码新闻项可能会抛出错误,而这些错误不会在任务内部处理,因此我们需要使用withThrowingTaskGroup()
来创建组。 - One of the main advantages of task groups is being able to add tasks inside a loop – we can loop from 1 through 5 and call
addTask()
repeatedly.
任务组的主要优势之一是能够在循环中添加任务——我们可以从 1 到 5 循环并重复调用addTask()。
- We wait for various items to complete in whatever order they finish, adding their contents to an
allStories
array.
我们等待各种项目以任何顺序完成,将它们的内容添加到allStories
数组中。 - As I said earlier, tasks in a group can complete in any order, so we sorted the resulting array of news stories to get them all in a sensible order.
正如我之前所说,一个组中的任务可以按任何顺序完成,因此我们对生成的新闻报道数组进行了排序,以合理的顺序将它们全部排序。
Regardless of whether you’re using throwing or non-throwing tasks, all tasks in a group must complete before the group returns. You have three options here:
无论您使用的是引发任务还是非引发任务,组中的所有任务都必须在该组返回之前完成。您有三个选项:
- Awaiting all individual tasks in the group.
正在等待组中的所有单个任务。 - Calling
waitForAll()
will automatically wait for tasks you have not explicitly awaited, discarding any results they return.
调用waitForAll()
将自动等待您未明确等待的任务,并丢弃它们返回的任何结果。 - If you do not explicitly await any child tasks, they will be implicitly awaited – Swift will wait for them anyway, even if you aren’t using their return values.
如果您没有明确等待任何子任务,它们将被隐式等待 – Swift 无论如何都会等待它们,即使您没有使用它们的返回值。
Of the three, I find myself using the first most often because it’s the most explicit – you aren’t leaving folks wondering why some or all of your tasks are launched then ignored.
在这三个选项中,我发现自己最常使用第一个,因为它是最明确的——你不会让人们想知道为什么你的部分或全部任务被启动然后被忽略。