说完了 async
和 await
,为了接下来的演示,我们先整理一下之前做饭的例子。首先,创建一些类型,分别表示食材 Food
和最后的菜品 Meal
:
enum Food {
case vegetable
case meat
}
struct Meal {}
其次,创建一个表示烤箱的类型 Oven
,它有一个“预热”的方法 preheatOven
以及一个烹饪的方法 cook
:
struct Oven {
func preheatOven() async {
print("Preheat oven.")
}
func cook(_ foods: [Food], seconds: Int) -> Meal {
print("Cook \(seconds) seconds.")
return Meal()
}
}
最后,把我们之前用过的 Dinner
改成下面这样:
struct Dinner {
func chopVegetable() async -> Food {
print("Chopping vegetables")
return .vegetable
}
func marinateMeat() async -> Food {
print("Marinate meat")
return .meat
}
}
做好这些准备之后,我们来写一个制作晚餐的函数 makeDinner
:
func makeDinner() async -> Meal {
let dinner = Dinner()
let veggies = await dinner.chopVegetable()
let meat = await dinner.marinateMeat()
let oven = Oven()
await oven.preheatOven()
let meal = Oven().cook([veggies, meat], seconds: 300)
return meal
}
然后调用它:
@main
struct MyApp {
static func main() async {
_ = await makeDinner()
}
}
就会看到下面这样的结果:
Chopping vegetables
Marinate meat
Preheat oven.
Cook 300 seconds.
在 makeDinner
的实现里,虽然 chopVegetable
,marinateMeat
,preHeatOven
都是异步方法,但 makeDinner
的执行过程却是“同步”的,只有切完菜才能腌肉,只有腌完肉才能预热烤箱。这显然是不太合理的,这三个步骤完全可以并行完成。这里只有一个操作是需要等待的,就是 cook
。只有切完菜、腌完肉并且烤箱已经预热好了之后,才能够进行烤制。
那么,该如何实现上面这个工作模式呢?这里,就要提到 Swift 结构化并发模型的第一部分内容:task 和 task group。每一个要并发执行的任务,叫做一个 task,例如上面提到的切菜、腌肉和预热烤箱。Task 在 Swift 中的呈现形式,通常就是异步函数。为了让任务并发执行,并等待它们完成的结果,我们需要创建一个 task group。这个 group 可以理解为一个任务的作用域,在离开这个作用域之前,它所有的任务都必须完成,同一个 group 中的任何一个任务都不可能脱离它的作用域存在。
理解了这两个概念之后,来看下面的代码:
func makeDinnerWithTaskGroup() async -> Meal {
var foods: [Food] = []
let oven = Oven()
await withTaskGroup(of: Food.self) { group in
let dinner = Dinner()
group.async {
await dinner.chopVegetable()
}
group.async {
await dinner.marinateMeat()
}
for await food in group {
foods.append(food)
}
}
await oven.preheatOven()
return await oven.cook(foods, seconds: 300)
}
这里,我们用泛型函数 withTaskGroup
创建一个 task group。它的第一个参数 of
表示这个 group 里的任务完成后返回值的类型。第二个参数是一个 closure,它接受一个 TaskGroup
类型的参数,这个参数有两个作用:
- 一个是用它的
async
方法在 task group 中创建要并行执行的任务。现在,切菜和腌肉就可以并行执行了; - 另外,由于
TaskGroup
是一个实现了AsyncSequence
的类型,我们可以通过等待它来获取所有异步任务的执行结果;
这样一来,withTaskGroup
完成后,我们就可以确认 foods
中包含了烹饪需要的所有食材。然后,我们就可以预热烤箱并且烹饪了。
不过,看到这里你可能会觉得,其实 preheatOven
也完全可以和准备食材并行进行呀。实际情况当然是这样的,但由于 preheatOven
的返回值和准备食材的方法不同,我们不能把它和切菜以及腌肉的方法放到一个 task group 里。为了实现这个效果,我们可以创建一个更大的 task group,然后把准备食材的过程,作为一个 sub task group:
func makeDinnerWithTaskGroup() async -> Meal {
var foods: [Food] = []
let oven = Oven()
await withTaskGroup(of: Void.self) {
$0.async {
await oven.preheatOven()
}
await withTaskGroup(of: Food.self) { group in
let dinner = Dinner()
group.async {
await dinner.chopVegetable()
}
group.async {
await dinner.marinateMeat()
}
for await food in group {
foods.append(food)
}
}
}
return await oven.cook(foods, seconds: 300)
}
这样,cook
之前的所有准备工作,就是完全异步的了。多调用几次 makeDinnerWithTaskGroup
,理论上应该可以在控制台看到准备工作的打印顺序是不同的。
What's next?
看到这里,虽然我们实现了预期的功能,但你一定和我一样对于 task group 中的两个回调函数嵌套耿耿于怀,我们不是为了去掉嵌套才学习这些技术的么,难道又要用回去了么?下一节,我们分享一个基于 enum
优化 makeDinnerWithTaskGroup
实现的方法。