# Swift Concurrency Part 2 --- ## 大綱(1/2) * Unstructured Task * Task * Cancellation * Task Cancellation * Cancellation handlers * Task Piority * Attached Task * Executor * ~~UnsafeCurrentTask~~ ---- ## 大綱(2/2) * Structured Task * TaskGroup * async let * Child Task --- ## Task ---- #### Concurrency 中最基礎的單元 ```swift= Task { // do something... } ``` ---- #### 所有 異步函數 皆執行在 `Task` 內 ```swift= // Task { await something() // } ``` --- ### Task State ```graphviz digraph Task { rankdir = LR; suspended -> running [label = "schedule"] running -> completed [label = "return/throw"] running -> suspended [label = "await"] } ``` ---- ### Task State 實際 > await 是潛在`暫停點` ```graphviz digraph Task { rankdir = LR; suspended -> running [label = "schedule"] running -> completed [label = "return/throw"] running -> suspended [label = "await"] running -> running [label = "await"] } ``` ---- ### 同步函數 ```swift= func main() { all() } func all() { a1() a2() a3() } ``` ```mermaid gantt title all dateFormat YYYY-MM-DD ss axisFormat %S section Main Thread a2 :a2, after a1, 2s a3 :a3, after a2, 2s a1 :a1, 2000-01-01 00, 2s all :all, 2000-01-01 00, 6s main :main, 2000-01-01 00, 6s ``` ---- ### call stack ![](https://i.imgur.com/vdNBhaQ.png) ---- ### 異步函數1 ```swift= func all() async { a1() await a2() a3() } ``` ---- ### 異步函數1-1 > 可在任意 thread 切換執行(executor) - Task: all - Job: a1 a2 a3 ```mermaid gantt title all dateFormat YYYY-MM-DD ss axisFormat %S section Global Executor1 a1 :a1, 2000-01-01 00, 2s section Global Executor2 a2 :a2, after a1, 2s section Global Executor3 a3 :a3, after a2, 2s ``` ---- ### 異步函數1-2 > await 是潛在暫停點,也有機會不暫停 - Task: all - Job: all ![](https://i.imgur.com/ejbIqvv.png) ```mermaid gantt title all dateFormat YYYY-MM-DD ss axisFormat %S section Global Executor all :2000-01-01 00, 6s ``` ---- ### 異步函數2(with actor) ```swift= actor A { let b = B() func all() async { a1() await b.a2() a3() } } actor B { func a2() {} } ``` > 每個 actor 有各自的 executor ```mermaid gantt title all dateFormat YYYY-MM-DD ss axisFormat %S section A a1 :a1, 2000-01-01 00, 2s a3 :a3, after a2, 2s section B a2 :a2, after a1, 2s ``` --- ### Task is Cooperative > Task Cancellation ---- #### Task Cancellation > 只標記 Task 為取消 ```swift= task.cancel() // task.isCancelled = true ``` ---- ```swift= Task { while !Task.isCancelled { // your works } return } ``` ---- ```swift= func doSomething() async { while !Task.isCancelled { // your works } return } ``` ---- ```swift= Task { try Task.checkCancellation() } ``` ---- #### 上次的小結 cancel 只能停止 * ~~尚未開始的 Task~~ * 有支援取消流程的 Task --- ### Default Executor 預設情況下 > `Task` 排程到 `default global concurrent executor` --- ## Executor ---- - Executor 是一個服務(service) - 接收提交過來的 Jobs 並且 調度 Thread 來執行 Job - 系統假設 executor 是可靠且執行 Job 時永不失敗 ---- - Executor 執行時是獨佔的(exclusive),表示同時多個 job 提交時,並不會`併發執行` - 不應該依照提交順序執行 - 實際上應以, 優先權 > 提交順序(submission order) ---- - Swift 提供預設 Executor 實作 ```swift= actor SomeActor {} ``` ---- - actor 以及 global actor 可替換其實作 ```swift= @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public protocol Actor : AnyObject, Sendable { nonisolated var unownedExecutor: UnownedSerialExecutor { get } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public protocol GlobalActor { // ... static var sharedUnownedExecutor: UnownedSerialExecutor { get } } ``` --- ## Cancellation handlers ---- ### 一般 Task cancel 需要自行實作 `cancel 流程` ---- ## TaskCancellationHandler 在 Task 被取消時,立即呼叫 `cancel handler` ---- ```swift= func download(url: URL) async throws -> Data? { var urlSessionTask: URLSessionTask? return try withTaskCancellationHandler { return try await withUnsafeThrowingContinuation { continuation in urlSessionTask = URLSession.shared.dataTask(with: url) { data, _, error in if let error = error { // Ideally translate NSURLErrorCancelled to CancellationError here continuation.resume(throwing: error) } else { continuation.resume(returning: data) } } urlSessionTask?.resume() } } onCancel: { urlSessionTask?.cancel() // runs immediately when cancelled } } ``` ---- ### Alamofire + Rx ```swift= public var data: Observable<Data> { return Observable.create { observer -> Disposable in self.dataRequest.responseData { (res) in do { try observer.onNext(res.result.get()) observer.onCompleted() } catch { observer.onError(error) } return Disposables.create { self.dataRequest.cancel() } } } } ``` ---- ### Alamofire + Continuation + TaskCancellationHandler ```swift= public var data: Data { get async throws { return try withTaskCancellationHandler { return try await withCheckedThrowingContinuation { continuation in self.dataRequest.responseData { (res) in do { let value = try res.result.get() continuation.resume(returning: value) } catch { continuation.resume(throwing: error) } } } } onCancel: { self.dataRequest.cancel() } } } ``` --- ## Attached Task ```swift= Task { // ... } ``` ---- ### Context inheritance * 跑在 Task * 沒跑在 Task ---- #### 跑在 Task * 繼承 `TaskPriority` ```swift= func xxx() async { Task { // 繼承 xxx `TaskPriority` } } ``` ---- #### 跑在 Task * `TaskLocal` values * copy value to inner task ```swift= @TaskLocal static var traceID: TraceID? print("traceID: \(traceID)") // traceID: nil $traceID.withValue(1234) { // bind the value print("traceID: \(traceID)") // traceID: 1234 } ``` ---- #### 跑在 Task & 跑在 Actor ```swift= actor One { var counter = 1 // mutable properties var notSendable = NotSendable() // non-sendable values func xxx() { Task { // 繼承 actor 的 Execution Context // 此 task 排程到 actor's executer // closure 變成 `actor-isolated` // 可以存取 actor 的私有狀態(`actor-isolated` state) // mutable properties // non-sendable values counter += 1 notSendable.xxx() } } } ``` ---- #### 沒跑在 Task ```swift= func xxx() { // Main Thread Task { // 諮詢 `runtime` 並推導出最可能的 `TaskPriority` // 例如問 `current thread priority` // 排程到 `global concurrent executor` } } ``` ---- ### Actor context propagation 傳遞給 `Task init` 的閉包將隱式繼承 `actor` `execution context` 和形成閉包的上下文的隔離 ```swift= func notOnActor(_: @Sendable () async -> Void) { } actor A { func f() { notOnActor { await g() // must call g asynchronously, because it's a @Sendable closure } Task { g() // okay to call g synchronously, even though it's @Sendable } } func g() { } } ``` ---- ### Implicit "self" - 要求使用 `self` 的意圖,是為了提醒開發者 `capture self` 會有潛在 `reference cycle` - 傳遞給 `Task init` 的閉包會被立刻執行,而且只會在內部 reference,因此 `explicit self` - 沒有傳達有效資訊 - 不需要(not be required) ---- ```swift= func acceptEscaping(_: @escaping () -> Void) { } class C { var counter: Int = 0 func f() { acceptEscaping { counter = counter + 1 // error: must use "self." because the closure escapes } Task { counter = counter + 1 // okay: implicit "self" is allowed here } } } ``` --- ## TaskPriority `Task` 的 `Priority` 是用來協助 `Executor` 做出 排程決策(scheduling decisions) ---- ### List * `high` * `medium` * `low` * `userInitiated` * `utility` * `background` ---- #### 命名源由 避免使用特定平台相關術語 採用較通俗的術語 ---- ##### C# ![](https://i.imgur.com/mBXJRVf.png) ---- ##### Thread ![](https://i.imgur.com/0YPgkd9.png) ---- ![](https://i.imgur.com/Zrg5BWy.png) ---- ##### Operation ![](https://i.imgur.com/y7F6Lad.png) ---- ##### GCD(OLD) ![](https://i.imgur.com/2BDWm2k.png) ---- ##### GCD(NEW) ![](https://i.imgur.com/GcclyGU.png) ---- ### 各個優先權處理方式,交由以下決定 * 各個平台 * specific executor ---- ### 優先權繼承 * child 繼承 parent 優先權 * Detached task 無繼承對象 ---- ### Task Priority == Executor Priority? - Task 優先權 不必匹配同等優先權的 Executor - 例子 - UI Thread: 高優先權 Executor - 任意 Task 提交至 UI Thread,在執行期間會以高優先權執行 --- ## Priority Escalation > 在某些情境下 `Task` 的 `Pirority` 必須被提升 > 以避免 `Priority Inversion` ---- ### [Priority inversion](https://en.wikipedia.org/wiki/Priority_inversion) * Disabling all interrupts to protect critical sections * Priority ceiling protocol * Priority inheritance * Random boosting * Avoid blocking ---- ### 優先級繼承 > [Priority inheritance](https://zh.wikipedia.org/wiki/%E4%BC%98%E5%85%88%E7%BA%A7%E7%BB%A7%E6%89%BF) ---- - 如果一個 Task A 在 Actor 上執行, - 同時有更高優先權的 Task B 已被排入 Actor 這個 Task A 可能暫時會以 `高優先權 Task B 同等`的`優先權`執行 - 這並不影響子任務或 reported priority - 優先權是 Task 跑在 Thread 的一項參數,而非 Task 本身 ---- - 如果 Unstructured Task A 建立 - 然後擁有更高優先權的 Task B 正等待 Task A 完成 - 此時 Task A 的優先權會永久性的提升至 Task B --- ## Structured Task > Parent - Child Task --- ### Sequential Execute ```swift= func chopVegetables() async throws -> [Vegetable] { ... } func marinateMeat() async -> Meat { ... } func preheatOven(temperature: Double) async throws -> Oven { ... } // ... func makeDinner() async throws -> Meal { let veggies = try await chopVegetables() let meat = await marinateMeat() let oven = try await preheatOven(temperature: 350) let dish = Dish(ingredients: [veggies, meat]) return try await oven.cook(dish, duration: .hours(3)) } ``` ---- ```mermaid gantt title Dinner dateFormat YYYY-MM-DD ss axisFormat %S section 廚房 Vegetables :Vegetables, 2000-01-01 00, 2s Meat :Meat, after Vegetables, 4s Oven :Oven, after Meat, 6s Dish :Dish, after Oven, 1s Dinner :after Dish, 3s ``` --- ### TaskGroup ```swift= @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @inlinable public func withTaskGroup<ChildTaskResult, GroupResult>( of childTaskResultType: ChildTaskResult.Type, returning returnType: GroupResult.Type = GroupResult.self, body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult ) async -> GroupResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @inlinable public func withThrowingTaskGroup<ChildTaskResult, GroupResult>( of childTaskResultType: ChildTaskResult.Type, returning returnType: GroupResult.Type = GroupResult.self, body: (inout ThrowingTaskGroup<ChildTaskResult, Error>) async throws -> GroupResult ) async rethrows -> GroupResult ``` ---- ### Add Child Task ```swift= @frozen public struct TaskGroup<ChildTaskResult> { public mutating func addTask( priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> ChildTaskResult ) public mutating func addTaskUnlessCancelled( priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> ChildTaskResult ) -> Bool } ``` ---- ### Add Child Task * Group 已取消時 ```swift= // addTaskUnlessCancelled -> false // 則不加入新的 child task ``` ---- ### Add Child Task * Group 已取消時 * 還是能加入新 Child Task * 因為取消流程 <iframe width="100%" height="300" frameborder="0" src="https://swiftfiddle.com/4jp6oeohjzbybk4jn6mn3x56gu/embedded/"> </iframe> ---- ### Query * 取得順序 * 不是按加入時的先後次序 * 先執行完畢的會優先返回 ```swift= for await value in group { // ... } ``` ---- ```swift= // 等待所有 child task public mutating func waitForAll() async // 有沒有未執行(pending) task public var isEmpty: Bool { get } ``` ---- ### TaskGroup cancel ```swift= // group.isCancelled = true public func cancelAll() public var isCancelled: Bool { get } ``` ---- ### Concurrent Execute(TaskGroup) ```swift= 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 } } } // If execution resumes normally after `withTaskGroup`, then we can assume // that all child tasks added to the group completed successfully. That means // we can confidently force-unwrap the variables containing the child task // results here. let dish = Dish(ingredients: [veggies!, meat!]) return try await oven!.cook(dish, duration: .hours(3)) } ``` ---- ```mermaid gantt title Dinner dateFormat YYYY-MM-DD ss axisFormat %S section 廚房 Vegetables :Vegetables, 2000-01-01 00, 2s Meat :Meat, 2000-01-01 00, 4s Oven :Oven, 2000-01-01 00, 6s Dish :Dish, after Oven, 1s Dinner :after Dish, 3s ``` --- ## async let ---- ```swift= let x = await xxx() let y = await yyy() ``` ---- ```swift= async let x = xxx() async let y = yyy() await (x, y) ``` ---- ### Concurrent Execute(async let) ```swift= func chopVegetables() async throws -> [Vegetable] { ... } func marinateMeat() async -> Meat { ... } func preheatOven(temperature: Double) async throws -> Oven { ... } // ... func makeDinner() async throws -> Meal { async let veggies = chopVegetables() async let meat = marinateMeat() async let oven = preheatOven(temperature: 350) let dish = await Dish(ingredients: [veggies, meat]) return try await oven.cook(dish, duration: .hours(3)) } ``` ---- ```mermaid gantt title Dinner dateFormat YYYY-MM-DD ss axisFormat %S section 廚房 Vegetables :Vegetables, 2000-01-01 00, 2s Meat :Meat, 2000-01-01 00, 4s Dish :Dish, after Meat, 1s Oven :Oven, 2000-01-01 00, 6s Dinner :after Oven, 3s ``` ---- ### 問題 `go` 執行多久? <iframe width="100%" height="300" frameborder="0" src="https://swiftfiddle.com/sw4jkgdsdbf2zn26alyw5a6jgu/embedded/"> </iframe> ---- ### 隱式 await ```swift= func go() async { async let f = fast() // 1s async let s = slow() // 3s // implicitly: cancels f // implicitly: cancels s // implicitly: await f // implicitly: await s return "nevermind..." } ``` ---- ### `async let` in closure 不能被 `@escaping` capture > 目前似乎不支援 ---- <iframe width="100%" height="300" frameborder="0" src="https://swiftfiddle.com/l6tlndzey5bvlkr3wyk2zkwpgy/embedded/"> </iframe> ---- ### Error Propagation 隱式: 取消 -> await -> 丟棄 error ```swift= func go() async -> String { async let f = fire() // f.cancel() // try? await f return "go" } ``` <iframe width="100%" height="300" frameborder="0" src="https://swiftfiddle.com/hqgs7sn6pvftndtdgc3qxhul6u/embedded/"> </iframe> ---- ### Cancellation 當 `Parent Task` 被取消,後續建立的 `Child Task` 也會被標記為 `取消狀態` <iframe width="100%" height="300" frameborder="0" src="https://swiftfiddle.com/5kiakqilnrfmnixkfjnmg6kheu/embedded/"> </iframe> --- ## Child Task ---- > Unstructured tasks are not able to utilize some of the optimization techniques wrt. --- ### Child Task 執行範圍的界線 > 限制在 Parent Task 內 ---- ### 何謂限制在 Parent Task 內 ```swift= group.add { sleep(1) ;return 1} group.add { sleep(100);return 100} return try await group.next() // return 1 ``` ```mermaid gantt title group dateFormat YYYY-MM-DD ss axisFormat %S section Child child1 :c1, 2000-01-01 00, 1s child2 :c2, 2000-01-01 00, 10s section Parent Parent :p1, 2000-01-01 00, 1s ``` ---- ### Child Task 執行範圍的界線 > Parent Task 必須等待所有 Child Task 執行完畢 ---- > X ```mermaid gantt title group dateFormat YYYY-MM-DD ss axisFormat %S section Child child1 :c1, 2000-01-01 00, 1s child2 :c2, 2000-01-01 00, 10s section Parent Parent :p1, 2000-01-01 00, 1s ``` > O ```mermaid gantt title group dateFormat YYYY-MM-DD ss axisFormat %S section Child child1 :c1, 2000-01-01 00, 1s child2 :c2, 2000-01-01 00, 10s section Parent Parent :p1, 2000-01-01 00, 10s ``` ---- ### 簡單範例 ```swift= group.add { sleep(1) ;return 1} group.add { sleep(100);return 100} // group.waitForAll() return try await group.next() // 結果 // sleep 100 秒 // return 1 // // 等待所有 child task 結束 // 才能 return ``` ---- ### Child Task 界線(Demo) <iframe width="100%" height="300" frameborder="0" src="https://swiftfiddle.com/lfns45s2yvdddmyavcmj4x5pui/embedded/"> </iframe> --- ### 錯誤傳遞 > Child Task 可以將錯誤往上拋給 Parent Task ---- ### 錯誤傳遞 * Parent Task handle error * `do catch` * `try` 拋出 error * 觸發 cancel all * 觸發結束(等待所有 Child Task) ---- ### 錯誤傳遞(Demo) <iframe width="100%" height="300" frameborder="0" src="https://swiftfiddle.com/zf3sglirxzdmjhomgo5ttkaxgq/embedded/"> </iframe> ---- ### 很重要 > cancel task,只有支援你有寫取消流程 > > cancel task,只有支援你有寫取消流程 > > > cancel task,只有支援你有寫取消流程 ---- ### 沒有取消流程的 Task 無法被 cancel --- ## `async let` ## vs ## `TaskGroup` ---- ### `async let` 無法對 `array` 使用 ```swift= for i in items.indices { group.async { (i, await f(items[i])) } } async let f0 = f(items[0]) async let f1 = f(items[1]) ``` ---- `TaskGroup` 可按照 `completion order` 取得結果 ---- `async let` return 時 > 會隱式 cancel --- ### Reference * [0304-structured-concurrency.md](https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md) * [0317-async-let.md](https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md) --- ## 致謝 stevapple 校稿, 流程等等建議
{"title":"Swift Concurrency Part2","tags":"swift","slideOptions":{"theme":"league","transition":"fade"}}
    565 views