上一节,虽然我们实现了预期的功能,不过你可能会实现方式感到有些不满。毕竟我们的目标是尽可能避免回调函数嵌套,结果居然在 task group 上又走回了嵌套的路数,这不禁让我们有种终日打雁,却被燕啄了眼的感觉。
怎么能优化一下呢?这里,分享一种基于 enum
的方法。之所以上一节采取了嵌套 task group 的方式,本质原因就是处理食材和预热烤箱的方法返回值类型不同。我们只要把这种不同,用 enum
的两个 case
区分开就好了。
enum Preparation {
case ingredient(Food)
case device
}
其中,ingredient
表示食材的准备,device
表示对设备的准备,由于我们只有烤箱,这里就不定义关联值了。
接下来,我们要让处理食材和预热烤箱的方法,都返回 Preparation
:
func chopVegetable() async -> Preparation {
print("Chopping vegetables")
return .ingredient(.vegetable)
}
func marinateMeat() async -> Preparation {
print("Marinate meat")
return .ingredient(.meat)
}
func preheatOven() async -> Preparation {
print("Preheat oven.")
return .device
}
现在,我们就可以把这三个任务,放在同一个 task group 里了:
func makeDinnerWithTaskGroup() async -> Meal {
var foods: [Food] = []
let oven = Oven()
try await withTaskGroup(of: Preparation.self) {
group in
let dinner = Dinner()
group.async {
await dinner.chopVegetable()
}
group.async {
await dinner.marinateMeat()
}
group.async {
await oven.preheatOven()
}
while let prep = await group.next(),
case Preparation.ingredient(let food) = prep {
foods.append(food)
}
}
return await oven.cook(foods, seconds: 300)
}
这样调整过后,在处理任务结果的时候,我们用 while
替代了 for
,在每一次循环的时候用 group.next()
方法得到每个任务返回的 Preparation
对象,再从中提取出 Food
的时候,就把它加入 foods
数组。
What's next?
这样,我们的改造就完成了。至于这样的方式是好是坏,暂时还很难下结论。毕竟,简化异步结构的代码是以修改若干 API 返回值为代价的,这可能只在项目早起规划的时候更为可行。另外,Preparation
也为其它代码带来了额外的间接性。大家知道有这样一种套路,结合自己实际情况使用就好了。
说到这,关于什么是结构化并发模型,大家应该有一个较为具体的了解了。它最大的好处,就是让我们可以用并发任务的逻辑关系来编写异步代码。相比之前基于线程同步原语的编程方式,它明显更易于理解和调试。
接下来,我们扩展一下这个例子,来看看 task group 中如果有任务发生了错误怎么办,以及如何在 task group 中取消任务。