说完了 asyncawait,为了接下来的演示,我们先整理一下之前做饭的例子。首先,创建一些类型,分别表示食材 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 的实现里,虽然 chopVegetablemarinateMeatpreHeatOven 都是异步方法,但 makeDinner 的执行过程却是“同步”的,只有切完菜才能腌肉,只有腌完肉才能预热烤箱。这显然是不太合理的,这三个步骤完全可以并行完成。这里只有一个操作是需要等待的,就是 cook。只有切完菜、腌完肉并且烤箱已经预热好了之后,才能够进行烤制。

那么,该如何实现上面这个工作模式呢?这里,就要提到 Swift 结构化并发模型的第一部分内容:tasktask 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 实现的方法。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们