Task of Swift's new concurrency framework

This article is the fourth in the series "New Concurrency Framework in Swift", which mainly introduces Task-based structured concurrency and unstructured tasks.

This series of articles introduces the content involved in Swift's new concurrency framework one by one, as follows:

This article was also published on my personal blog

Overview


The first three articles introduced async/await for synchronizing asynchronous code , actor of concurrency security model, and Sendable for constraining the safe passing of values ​​in a concurrent environment .

Strictly speaking, they do not have the ability to provide "concurrency", but provide some basic auxiliary functions for concurrency.

The protagonist Task of this article can provide the ability of "concurrent execution".

Task


A few key points:

  • The basic unit ("block of code") that executes a task in a concurrent environment;

  • All asynchronous functions (async) run in Task;

  • Task belongs to a higher-level abstraction above thread, and the system is responsible for scheduling and executing Task on the appropriate thread.

Task has 3 states:

  • suspended (suspended) — There are 2 situations that cause a Task to be suspended:

    • Task is ready to wait for the system to allocate execution threads;
    • Waiting for external events, such as Task may enter a suspended state after encountering a suspension point and wait for external events to wake up.

    ps. It should be noted that when an asynchronous function ( A) calls another asynchronous function ( B), the caller will be suspended, which does not mean that the entire Task will be suspended.

    从函数 A 的视角看,其会暂停等待函数 B 返回;

    但从 Task 视角看,其不一定会暂停,可能会继续在其上执行被调用的函数 B

    当然,Task 也可能会被暂停,如果被调用的函数要在不同的并发上下文中执行。

  • 运行中 (running) — Task 当前正在某个线程上运行,直至完成,或遇到 suspension point 而进入暂停状态;

  • 已完成 (completed) — Task 所有工作都已完成。

总之,Task 是线程的高级抽象,用于执行一项任务。

Task 提供了一些高级抽象能力:

  • Task 可以携带调度信息,如:任务优先级;

  • Task 作为正在执行的任务的句柄 (Handle),可以用于 cancel 等;

  • Task 可以携带用户提供的 task-local data。

Structured concurrency


Structured concurrency,结构化并发,听起来挺玄乎。

说白了,就是在 Task 间可以有父子关系,并形成一颗「Task tree」:

Tasktree.png

通过 Task 间的父子关系可以更好地对一组 Task 进行管理:

  • 子 Task 的生命周期不会超出父 Task 的范围 (这点非常重要);

  • cancel 更便捷 (cancel 某个 Task 时,其所有子 Task 也会被 cancel);

  • 错误处理更方便了,未处理的 error 会自动从子 Task 传播到父 Task;

  • 子 Task 默认会继承父 Task 的优先级;

  • 父子 Task 间会共享 Task-local data;

  • 父 Task 可以很容易收集子 Task 的结果。

以上就是结构化并发的全部!

下面,就其中的细节逐一展开讨论。

目前,实现结构化并发有 2 种方式:

  • async let

  • Task group。

async let

1  // given: 
2  //   func chopVegetables() async throws -> [Vegetables]
3  //   func marinateMeat() async -> Meat
4  //   func preheatOven(temperature: Int) async -> Oven
5  //
6  func makeDinner() async throws -> Meal {
7    async let veggies = chopVegetables()
8    async let meat = marinateMeat()
9    async let oven = preheatOven(temperature: 350)
10
11   let dish = Dish(ingredients: await [try veggies, meat])
12   return try await oven.cook(dish, duration: .hours(3))
13 }
复制代码

先通过一个例子感受一下,几个关键点:

  • 对异步函数的调用不用 await,而是在赋值表达式的最左边加上 async let (第 7~8 行),称之为 async let binding

  • 在需要使用 async let 表达式的结果时要用 await,如结果可能会抛出错误,还需要处理错误 (第 11~12 行);

  • async let 只能出现在异步上下文中 (Task closure、async function 以及 async closure)。

上述例子来自:swift-evolution/0317-async-let.md at main · apple/swift-evolution · GitHub

以上是我们的直观感受,其背后的实现机制是:

  • 系统为每个 async let 创建一个并发的子任务;

  • 子任务创建后立马开始执行;

  • 子任务会继续父任务的优先级以及 task-local datas。

因此,如上例,会创建 3 个并发子任务分别执行 chopVegetablesmarinateMeat 以及 preheatOven

Implicit async let awaiting

有个问题:正常流程下,对 async let 需要执行 await 操作,如果不执行 await 会怎样呢?

会导致子任务溢出吗?(超出父任务的生命周期?)

答案是否定的。

1  func makeDinner() async throws -> Meal {
2    async let veggies = chopVegetables()
3    async let meat = marinateMeat()
4    async let oven = preheatOven(temperature: 350)
5  }
复制代码

如上代码,系统会添加隐式 cancel、await:

1  func makeDinner() async throws -> Meal {
2    async let veggies = chopVegetables()
3    async let meat = marinateMeat()
4    async let oven = preheatOven(temperature: 350)
5    // implicitly: cancel veggies
6    // implicitly: cancel meat
7    // implicitly: cancel oven
8    // implicitly: await veggies
9    // implicitly: await meat
10   // implicitly: await oven
11 }
复制代码

我们通过一个简单的例子验证一下上述结论:

1   func noAwaitAsynclet() async {
2     print("begin noAwaitAsynclet")
3     try? await Task.sleep(nanoseconds: 1_000_000_000)
4     Task.isCancelled ? print("noAwaitAsynclet is cancelled") : print("end noAwaitAsynclet")
5   }
6  
7   func testAsynclet() async {
8     let parentTask =
9     Task {
10      async let test = noAwaitAsynclet()
11    }
12    
13    await parentTask.value
14    print("parentTask finished!")
15  }
复制代码

调用 testAsynclet 方法的输出:

begin noAwaitAsynclet
noAwaitAsynclet is cancelled
parentTask finished!
复制代码

cancel

正如前文所述,在结构化并发中 cancel 操作会从父任务传递给所有子任务

1   func noAwaitAsynclet() async {
2     print("begin noAwaitAsynclet")
3     try? await Task.sleep(nanoseconds: 1_000_000_000)
4     Task.isCancelled ? print("noAwaitAsynclet is cancelled") : print("end noAwaitAsynclet")
5   }
6  
7   func testAsynclet() async {
8     let parentTask =
9     Task {
10      async let test = noAwaitAsynclet()
11      await test
12    }
13    
14    parentTask.cancel()
15    await parentTask.value
16    print("parentTask finished!")
17  }
复制代码

对前面那个例子简单改动一下:

  • 11 行添加对 testawait

  • 14 行对 parentTask 执行 cancel

其输出:

begin noAwaitAsynclet
noAwaitAsynclet is cancelled
parentTask finished!
复制代码

可以看到,对父任务的 cancel 操作传递到了 async let 子任务。

Task group

用 Task group 重写 makeDinner 来直观感受一下 Task group:

func makeDinner() async throws -> Meal {
  // Prepare some variables to receive results from our concurrent child tasks
  var veggies: [Vegetable]?
  var meat: Meat?
  var oven: Oven?

  enum CookingStep { 
    case veggies([Vegetable])
    case meat(Meat)
    case oven(Oven)
  }

  // Create a task group to scope the lifetime of our three child tasks
  try await withThrowingTaskGroup(of: CookingStep.self) { group in
    group.addTask {
      try await .veggies(chopVegetables())
    }
    group.addTask {
      await .meat(marinateMeat())
    }
    group.addTask {
      try await .oven(preheatOven(temperature: 350))
    }

    for try await finishedStep in group {
      switch finishedStep {
        case .veggies(let v): veggies = v
        case .meat(let m): meat = m
        case .oven(let o): oven = o
      }
    }
  }

  let dish = Dish(ingredients: [veggies!, meat!])
  return try await oven!.cook(dish, duration: .hours(3))
}
复制代码

几个关键点:

  • Task group 没有公开的 init 方法,只能通过 withTaskGroupwithThrowingTaskGroup 方法来获得 Task group 实例;

  • 通过 Task group 的 addTask 方法可以创建并发执行的子任务,且子任务的数量可以是动态的;

  • 同一 group 中所有子任务的结果类型必须相同;

    上例是通过 enum (CookingStep)封装关联值的方式使得所有子任务结果类型相同的。

  • 子任务的生命周期不会超出 group 生命周期;

    因此当 group(withTaskGroupwithThrowingTaskGroup) 方法返回时就意味着所有子任务都已完成或 cancel;

  • 通过 for await ... in 可以遍历所有子任务的运行结果;

    需要注意的是遍历的顺序是子任务完成的顺序,而非子任务添加的顺序;

  • 当 group 内部抛出错误时 (如某个子任务抛出异常),所有未完成的子任务都将被 cancel。

如下,如果在 group 内不显式地等待所有子任务完成,会如何?

try await withThrowingTaskGroup(of: CookingStep.self) { group in
  group.addTask {
    try await .veggies(chopVegetables())
  }
  group.addTask {
    await .meat(marinateMeat())
  }
  group.addTask {
    try await .oven(preheatOven(temperature: 350))
  }
}
复制代码

group 还是会隐式的等待所有子任务完成才返回

注意此处与 async let 的区别,如上文所述,async let 子任务会先被 cancel,再 await。

async let vs. Task group

async let 与 Task group 同属结构化并发范畴,在日常开发中如何选择?

基本原则:能用 async let 就不用 Task group。

由两个版本的 makeDinner 方法可以看出:

  • async let 更轻量、更直观;

  • Task group 要求所有子任务的计算结果类型相同,往往需要多一层封装,如 makeDinner 中的 CookingStep枚举。同时,Task group 接口是基于 closure 的,也进一步导致代码变复杂。

那有什么是 Task group 可以做,而 async let 无法做到的?

主要有 2 点:

  • async let 创建子任务的数量是静态的,而 Task group 可以动态创建子任务;

    如下,loadImages 方法为每个 url 创建一个下载图片的子任务,其数量由参数 urls 动态决定:

    func loadImages(urls: [String]) async -> [Image] {
      await withTaskGroup(of: Image.self, body: { group in
        for url in urls {
          group.addTask {
            return await downloadImage(url: url)
          }
        }
    
        var images: [Image] = []
        for await image in group {
          images.append(image)
        }
    
        return images
      })
    }
    复制代码
  • async let 等待子任务完成的顺序是固定,无法做到按子任务完成顺序取结果。

    如下,无论 3 个子任务哪个先完成,我们一定是先获得 veggiesValue,再获得 meatValue,最后获取 ovenValue

    1  func makeDinner() async throws -> Meal {
    2    async let veggies = chopVegetables()
    3    async let meat = marinateMeat()
    4    async let oven = preheatOven(temperature: 350)
    5    let veggiesValue = await veggies
    6    let meatValue = await meat
    7    let ovenValue = await oven
    8 }
    复制代码

    而 Task group 是以子任务完成的顺序拿到结果的。

    这有什么用吗?

    func fastestResponse() async -> Int {
      await withTaskGroup(of: Int.self, body: { group in
        group.addTask {
          let _ = await requestFromServer1()
          return 1
        }
    
        group.addTask {
          let _ = await requestFromServer2()
          return 2
        }
    
        return await group.next()!
      })
    }
    复制代码

    如上,有两台布署了相同服务的服务器,需要确定当前哪台服务器响应速度更快。

    通过 Task group 按子任务完成顺序返回的特性很容易就能实现。

小结

通过上文讨论,我们知道结构化并发有很多优势。

其中,最重要的一条是:子任务的生命周期不会超出父任务。

其使得我们可以很容易做到:

  • 控制一组任务,如 cancel,只要对父任务执行 cancel,其中的所有子任务都会被 cancel;

    如果子任务的生命周期比父任务长,就很难做到这一点。因为在需要执行 cancel 时,父任务可能已经结束了。

  • 等待一组任务完成,只要等待父任务完成即可,因为父任务完成就意味着所有子任务都已完成;

  • 配合 async/await 可以很容易地实现多组任务间的依赖。

要在传统并发模型中实现以上需求往往需大费周章。

Unstructured tasks


非结构化任务,简单讲,就是任务间没有父子关系,不存在 「 Task tree 」。

通过上文我们知道,结构化并发最重要的特性就是子任务的生命周期不会超出父任务。

而非结构化任务就不存在这个约束。

有时只需要创建一个并发任务,或在同步上下文中为了调用异步方法而创建异步环境。

以上是非结构化任务的 2 个主要应用场景。

创建非结构化任务有 2 种方式:

  • Task.init

  • Task.detached

Task.init

@frozen public struct Task<Success, Failure> : Sendable where Success : Sendable, Failure : Error {}

extension Task where Failure == Error {
  public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success)
}
复制代码
let dinnerHandle = Task {
  try await makeDinner()
}

await dinnerHandle.value
dinnerHandle.cancel()
复制代码

如上,Task.init 返回一个 task 句柄 (dinnerHandle),通过该句柄可以获取任务执行的结果,也可以取消任务。

Context inheritance

通过 Task.init 创建的任务会从当前上下文中继承重要的元信息,如:

  • 任务优先级;

  • task-local data;

  • actor isolation。

如果 Task.init 是在异步上下文中调用的 (意味着调用链上存在 Task):

  • 新创建的任务会继承当前任务的优先级;

  • 通过拷贝的方式继承当前任务的所有 task-local data;

  • 如果是在 actor 方法中调用 Task.init 的,则 Task closure 将成为 actor-isolated。

    从上面 Task.init 定义可以知道,Task closure 是用 Sendable 修饰的。

    在「Swift 新并发框架之 Sendable」中介绍过,Sendable closure 是不能捕获 actor-isolated 属性,否则报错: Actor-isolated property 'x' can not be referenced from a Sendable closure。

    但 Task closure 是个例外,因为它本身也是 actor-isolated,所以下面的代码不会报错:

    public actor TestActor {
      var value: Int = 0
    
      func testTask() {
        Task {
          value = 1
        }
     }
    }
    复制代码

如果 Task.init 是在同步上下文中调用的 (调用链上没有 Task):

  • 运行时推断合理的优先级;

Task.detached

extension Task where Failure == Never {
  public static func detached(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success) -> Task<Success, Failure>
}
复制代码
let dinnerHandle = Task.detached {
  try await makeDinner()
}
复制代码

通过 Task.detached 创建的任务完全独立于当前上下文,也就是不会继承当前上下文的优先级、task-local data 以及 actor isolation。

小结


至此,基于 Task 创建任务的四种形态全部介绍完了。

Explore structured concurrency in Swift - WWDC21 中对它们有一个总结:

Flavorsoftasks.png

结构化并发可以说是一次重大进步,今后编码并发相关的代码会更加容易!

参考资料

swift-evolution/0296-async-await.md at main · apple/swift-evolution · GitHub

swift-evolution/0317-async-let.md at main · apple/swift-evolution · GitHub

swift-evolution/0302-concurrent-value-and-concurrent-closures.md at main · apple/swift-evolution · GitHub

swift-evolution/0337-support-incremental-migration-to-concurrency-checking.md at main · apple/swift-evolution · GitHub

swift-evolution/0304-structured-concurrency.md at main · apple/swift-evolution · GitHub

swift-evolution/0306-actors.md at main · apple/swift-evolution · GitHub

swift-evolution/0337-support-incremental-migration-to-concurrency-checking.md at main · apple/swift-evolution · GitHub

Understanding async/await in Swift • Andy Ibanez

Concurrency — The Swift Programming Language (Swift 5.6)

Connecting async/await to other Swift code | Swift by Sundell

Explore structured concurrency in Swift - WWDC21 - Videos - Apple Developer

Swift concurrency: Behind the scenes - WWDC21 - Videos - Apple Developer

Guess you like

Origin juejin.im/post/7084640887250092062