本文是 『 Swift 新并发结构 』系列文章的第四篇,首要介绍基于 Task 的结构化并发 (Structured concurrency) 和非结构化并发使命 (Unstructured tasks)。
本系列文章对 Swift 新并发结构中触及的内容逐一进行介绍,内容如下:
-
Swift 新并发结构之 async/await
-
Swift 新并发结构之 actor
-
Swift 新并发结构之 Sendable
-
Swift 新并发结构之 Task
本文一起宣布于我的个人博客
Overview
前三篇文章分别介绍了用于将异步代码同步化的 async/await、并发安全模型 actor 以及用于束缚在并发环境下能够安全传值的 Sendable。
严厉含义上说,它们并不具备供给「并发」的才能,而是为并发供给若干基础辅助功用。
本文主角 Task 则能供给「并发履行」的才能。
Task
几个要害点:
-
并发环境中履行使命的基本单元 (「代码块」);
-
一切的异步函数 (async) 都运转在 Task 内;
-
Task 归于线程之上的更高档笼统,由体系担任在适宜的线程上调度履行 Task。
Task 有 3 种状况:
-
暂停 (suspended) — 有 2 种状况会导致 Task 处于暂停状况:
-
Task 已准备就绪等候体系分配履行线程;
-
等候外部事情,如 Task 遇到 suspension point 后或许会进入暂停状况并等候外部事情来唤醒。
ps. 需求留意的是,异步函数 (
A
) 调用另一个异步函数 (B
)时,调用方会暂停,并不意味着整个 Task 会暂停。从函数
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」:
经过 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 个并发子使命分别履行 chopVegetables
、marinateMeat
以及 preheatOven
。
Implicitasync 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
行添加对test
的await
; -
第
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
办法,只能经过withTaskGroup
或withThrowingTaskGroup
办法来取得 Task group 实例; -
经过 Task group 的
addTask
办法能够创立并发履行的子使命,且子使命的数量能够是动态的; -
同一 group 中一切子使命的成果类型有必要相同;
上例是经过 enum (
CookingStep
)封装相关值的办法使得一切子使命成果类型相同的。 -
子使命的生命周期不会超出 group 生命周期;
因而当 group(
withTaskGroup
、withThrowingTaskGroup
) 办法回来时就意味着一切子使命都已完结或 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 中对它们有一个总结:
结构化并发能够说是一次严重前进,今后编码并发相关的代码会更加简略!
参考资料
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