理解Swift中的任务组
hudson 译 原文
苹果在Swift 5.5中引入了任务组,作为Swift并发框架的重要组成部分之一。顾名思义,任务组是并发运行的子任务的集合,只有当其所有子任务完成执行时,它才会返回。
在本文中,我想向您展示如何创建任务组,将子任务添加到任务组,并从所有子任务中收集结果。有很多话题要讨论,所以让我们开始吧。
准备工作
像往常一样,我将使用一些示例代码来解释并帮助您了解在Swift中任务组是如何工作的。在进入任务组之前,让我们先定义一个操作结构,我们将在本文中使用它。
swift
struct SlowDivideOperation {
let name: String
let a: Double
let b: Double
let sleepDuration: UInt64
func execute() async -> Double {
// Sleep for x seconds
await Task.sleep(sleepDuration * 1_000_000_000)
let value = a / b
return value
}
}
SlowDivideOperation
是一个简单的结构,它包含一个execute()
函数,该函数根据给定的数字执行除法操作。请注意,在除以数字之前,我故意让它睡眠一段时间,从而减慢执行速度。
这使我们能够很好地控制除法操作应该执行多长时间,。因此,我们可以轻松观察当多个SlowDivideOperation
在任务组中同时运行时会发生什么。
理解了这一点,让我们直接进入任务组。
任务组行为
为了使我们能够正确使用任务组,我们需要注意一些任务组行为:
-
任务组由相互独立的异步任务(子任务)的集合组成。
-
添加到任务组的所有子任务都会自动并发地开始运行。
-
我们无法控制子任务何时完成执行。因此,如果我们希望子任务按特定顺序完成,我们不应该使用任务组。
-
任务组仅在其所有子任务完成执行时返回。换句话说,任务组的所有子任务只能在其任务组的范围内。
-
任务组可以通过返回值、返回
void
(非值返回)或抛出错误来退出。
现在您已经了解了任务组的行为方式,是时候编写一些Swift代码了。
创建任务组
要创建任务组,我们可以使用Swift 5.5中引入的withTaskGroup(of:returning:body:)
或withThrowingTaskGroup(of:returning:body:)
函数。由于我们要创建的任务组不会抛出任何错误,我们将在示例代码中使用withTaskGroup(of:returning:body:)
变体。它被调用时采取以下形式:
在我们的示例中,我们将创建一个任务组,生成多个子任务,执行SlowDivideOperation
并返回其名称和结果。当所有SlowDivideOperation
完成后,任务组将收集其所有子任务结果,并返回一个由所有SlowDivideOperation
名称和结果组成的字典。
考虑到这一点,我们可以像这样使用函数创建一个任务组:
swift
let allResults = await withTaskGroup(of: (String, Double).self,
returning: [String: Double].self,
body: { taskGroup in
// 我们可以在这里使用`taskGroup`来创建子任务。
})
请注意,任务组的实例作为主体闭包的参数提供给我们。在下一节中,我们将了解如何利用此任务组实例来生成多个并发运行的子任务。
任务组如何工作
假设我们有一个SlowDivideOperation
数组,如下所示:
swift
let operations = [
SlowDivideOperation(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
SlowDivideOperation(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
SlowDivideOperation(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
]
然后,我们可以通过循环operations
数组将子任务添加到任务组中:
swift
let allResults = await withTaskGroup(of: (String, Double).self,
returning: [String: Double].self,
body: { taskGroup in
// Loop through operations array
for operation in operations {
// Add child task to task group
taskGroup.addTask {
// Execute slow operation
let value = await operation.execute()
// Return child task result
return (operation.name, value)
}
}
// Collect child task results here...
})
值得一提的是,我们在子任务操作闭包中返回String
和Double
,与我们之前设置的子任务结果数据类型匹配。
如前所述,所有子任务都将同时运行,我们无法控制它们何时完成运行。为了收集每个子任务的结果,我们必须像这样循环任务组:
swift
// Collect results of all child task in a dictionary
var childTaskResults = [String: Double]()
for await result in taskGroup {
// Set operation name as key and operation result as value
childTaskResults[result.0] = result.1
}
// Task group finish running & return task group result
return childTaskResults
请注意,我们在循环时使用等待关键字来指示for
循环在等待子任务完成时可能会暂停。每当子任务返回时,for
循环都会重复并更新childTaskResults
字典。
一旦所有子任务完成执行,for
循环将退出并继续返回任务组结果。正如您可能已经注意到的,childTaskResults
的数据类型必须与我们之前设置的任务组结果类型匹配。
提示:
如果您有任务组或子任务不返回值,请使用
Void.self
作为结果类型。
让我们在触发任务组之前和之后添加一些打印语句,以检查最终结果和执行所有子任务所需的时间。以下是完整的示例代码:
swift
let operations = [
SlowDivideOperation(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
SlowDivideOperation(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
SlowDivideOperation(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
]
Task {
print("Task start : \(Date())")
let allResults = await withTaskGroup(of: (String, Double).self,
returning: [String: Double].self,
body: { taskGroup in
// Loop through operations array
for operation in operations {
// Add child task to task group
taskGroup.addTask {
// Execute slow operation
let value = await operation.execute()
// Return child task result
return (operation.name, value)
}
}
// Collect results of all child task in a dictionary
var childTaskResults = [String: Double]()
for await result in taskGroup {
// Set operation name as key and operation result as value
childTaskResults[result.0] = result.1
}
// All child tasks finish running, thus task group result
return childTaskResults
})
print("Task end : \(Date())")
print("allResults : \(allResults)")
}
如果我们尝试执行上述代码,我们得到的输出将如下:
swift
Task start : 2021-10-23 05:53:15 +0000
Task end : 2021-10-23 05:53:20 +0000
allResults : ["operation-1": 2.0, "operation-2": 4.0, "operation-0": 5.0]
如您所见,整个任务组花了5秒才完成。事实上,5秒也等于所有SlowDivideOperation
中最长的睡眠时间,这表明所有子任务实际上都同时运行。
如果您查看所有结果,您会注意到它包含所有子任务的结果。这进一步证明,任务组只有在其所有子任务完成运行时才会返回。这也表明,子任务只能存在于任务组的上下文中。
如果您想尝试本文中的示例代码,您可以在这里获取。
感谢您的阅读。👨🏻💻