Swift Concurrency 學習筆記

語言: CN / TW / HK

Swift Concurrency 學習筆記

Swift 5.5裡新增了Swift Concurrency,語法和Web前端裡的非同步非常之像,語法學習起來比較簡單。

基本使用

關鍵詞就是asyncawait。不同的是需要放入Task裡執行,並且一定需要加await關鍵字。

```swift func fn() async { print("async function") }

Task { await fn() } ```

另外一種是可以丟擲錯誤的async函式。

swift func fn() async throws -> String{ throw URLError(.badURL) return "async" }

呼叫會丟擲錯誤的async函式的時候需要使用try關鍵字。

swift Task{ let result = try await fn() print(result) }

這樣是不會輸入任何結果的,因為已經丟擲錯誤了,在這種情況需要用do-catch語句。

swift Task { do { let result = try await fn() print(result) } catch { print(error.localizedDescription) // 輸出了錯誤資訊 } }

使用do-catch可以捕獲錯誤,另外還有2種try的修飾,try!try?,可以不使用do-catch

swift let result = try! await fn() // 程式會直接崩潰,不會走do-catch,捕獲不了錯誤 print(result) try!是非常不建議使用的。

swift let result = try? await fn() // 報錯會返回nil print(result) // nil

try?在出現錯誤的時候會返回nil,在不需要捕獲具體錯誤資訊的時候非常有用。

Task

Task接受一個閉包作為引數,返回一個例項。

取消 Task

Task會返回例項,通過該例項的cancel()方法可取消任務。

```swift func fn() async { try? await Task.sleep(for: .seconds(2)) print("async function") }

let task = Task { await fn() } task.cancel() ```

但是實際我們還是會輸出"async function",只是跳過了等待2秒。

所以我們需要呼叫Task.isCancelled或者Task.checkCancellation()來確保不再執行。

swift func fn() async { try? await Task.sleep(for: .seconds(2)) if Task.isCancelled { return } print("async function") }

Task的優先順序

Task中有優先順序的概念

Swift Task(priority: .background) { print("background: \(Task.currentPriority)") } Task(priority: .high) { print("high: \(Task.currentPriority)") } Task(priority: .low) { print("low: \(Task.currentPriority)") } Task(priority: .medium) { print("medium: \(Task.currentPriority)") } Task(priority: .userInitiated) { print("userInitiated: \(Task.currentPriority)") } Task(priority: .utility) { print("utility: \(Task.currentPriority)") }

輸出

medium: TaskPriority(rawValue: 21) high: TaskPriority(rawValue: 25) low: TaskPriority(rawValue: 17) userInitiated: TaskPriority(rawValue: 25) utility: TaskPriority(rawValue: 17) background: TaskPriority(rawValue: 9) 優先順序並不一定匹配,有時候會有優先順序提升的情況。

子任務會繼承父任務的優先順序。

swift Task(priority: .high) { Task { print(Task.currentPriority) // TaskPriority(rawValue: 25) } }

通過Task.detached來分離任務。

swift Task(priority: .high) { Task.detached { print(Task.currentPriority) // TaskPriority(rawValue: 21) } }

掛起Task

Task.yield()可以掛起當前任務。

```swift Task { print("task 1") } Task { print("task 2") }

// 輸出 // task 1 // task 2 ```

使用Task.yield()

swift Task { await Task.yield() print("task 1") } Task { print("task 2") } // 輸出 // task 2 // task 1

async let

await是阻塞的,意味著當前await函式在沒執行完之前是不會執行下一行的。

```swift func fn() async -> String { try? await Task.sleep(for: .seconds(2)) return "async function" }

Task { let result = await fn() print(result) // 等待兩秒後輸出async function } ```

有些情況需要並行執行多個async函式,這個時候則會用到async let

```swift Task { async let fn1 = fn() async let fn2 = fn()

let result = await [fn1, fn2]

print(result) // ["async function", "async function"]

} ```

TaskGroup

如果任務過多,或者是迴圈裡建立並行任務,async let就不是那麼得心應手了,這種情況我們應該使用withTaskGroupwithThrowingTaskGroup

swift Task { let string = await withTaskGroup(of: Int.self) { group in for i in 0 ... 10 { group.addTask { try? await Task.sleep(for: .seconds(2)) return i } } var collected = [Int]() for await value in group { collected.append(value) } return collected } print(string) }

of為子任務返回型別,在TaskGroup裡我們也能通過group.cancelAll()group.isCanceled配合來取消任務。

Continuations

Continuations用於將以前的非同步回撥函式變成async函式,類似前端裡的new Promise(resolve,reject)

現有以下程式碼

swift func fn(_ cb: @escaping (String) -> Void) { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { cb("completed") } }

這段程式碼是通過@escaping閉包的形式來獲取結果,不能通過await獲取,只需要使用withCheckedContinuation就可以將函式改造為async函式。

```swift func asyncFn() async -> String { await withCheckedContinuation { continuation in fn { continuation.resume(returning: $0) } } }

Task { let result = await asyncFn() print(result) } ```

除了withCheckedContinuation,還有withCheckedThrowingContinuation可以丟擲錯誤。

actor

在很多語言裡,都有執行緒鎖這個概念,避免多個執行緒同一時間訪問同一資料,造成錯誤。

Swift Concurrency裡通過actor來解決這個問題。actor裡的屬性和方法都是執行緒安全的。

```swift actor MyActor { var value:String = "test"

func printValue(){
    print(value)
}

} ```

actor內預設屬性和方法都是非同步的,需要通過await來呼叫。

swift Task { let myActor = MyActor() await myActor.printValue() print(await myActor.value) }

如果需要某個方法不用await呼叫,需要使用nonisolated關鍵字。

```swift actor MyActor { nonisolated func nonisolatedFn(){ print("nonisolated") } }

let myActor = MyActor() myActor.nonisolatedFn() ```

MainActor

現有以下程式碼

```swift class VM: ObservableObject { @Published var value = "value"

func change() {
    Task{
        try? await Task.sleep(for:.seconds(2))
        self.value = "change"
    }
}

}

Text(vm.value) .onTapGesture { vm.change() } ```

當點選Text兩秒後會修改值。這時候會提示。

[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates

因為UI改動都應該發生在主執行緒,可以使用老辦法Dispatch.main.async來解決。在Swift Concurrency裡有多個方法。

swift func change() { Task { try? await Task.sleep(for: .seconds(2)) await MainActor.run{ self.value = "change" } } }

或者

swift func change() { Task {@MainActor in try? await Task.sleep(for: .seconds(2)) self.value = "change" } }

也可以使用@MainActor將方法或者類標記執行在主佇列。

SwiftUI中使用

SwiftUI中直接.task修飾符即可。

swift Text("Hello World 🌍") .task { await fn() }

同時有一點比較好的是在onDisappear的時候會自動取消Task

結語

作為初學者,Swift Concurrency簡化了很多非同步相關的問題,不需要再去使用閉包了,不會造成回撥地獄,結合SwiftUI使用比Combine更簡單友好,非常不錯。

最近幾天學習了這個,雖然我陽了,但是還是頂著發燒總結一晚上,以免燒完已經不記得了。