Swift Concurrency (非同步程式碼)

Swift 5.5 引入了全新的 Concurrency (並發) 模型,讓寫非同步程式碼變得像寫同步程式碼一樣直觀、安全且高效。這是 Swift 語言發展史上最大的變革之一。

在此之前,我們通常使用 Closure (Callbacks) 或 Combine 來處理非同步工作,但這容易導致「Callback Hell」以及難以追蹤的執行緒安全問題。

核心概念

  1. async / await:定義與呼叫非同步函式的語法。
  2. Task:建立非同步工作的單元。
  3. Actor:保護可變狀態 (Mutable State) 當被多個執行緒同時存取時的安全性。

Async / Await

定義非同步函式

在函式參數後加上 async 關鍵字,表示這是一個非同步函式。如果有回傳值,寫在箭頭前。如果會拋出錯誤,寫 async throws

func fetchPhoto(url: String) async throws -> UIImage {
    // 模擬網路請求
    try await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 休息 1 秒
    return UIImage(named: "photo")!
}

呼叫非同步函式

呼叫 async 函式時,必須在其前方加上 await 關鍵字。這代表程式執行到這裡會暫停 (Suspend),直到非同步工作完成後才繼續往下執行。這期間執行緒可以去處理其他工作。

func showPhoto() async {
    do {
        let photo = try await fetchPhoto(url: "https://example.com/image.jpg")
        print("圖片下載完成")
        // 更新 UI
    } catch {
        print("下載失敗: \(error)")
    }
}
await 只能在非同步環境 (如另一個 async 函式或 Task 區塊) 中使用。

結構化並發 (Structured Concurrency)

Swift 允許你並行執行多個非同步工作。

Async Let

如果你想同時下載三張圖片,而不是一張接一張下載:

async func loadGallery() {
    // 這裡的三個下載任務會「同時」開始
    async let firstPhoto = fetchPhoto(url: "1.jpg")
    async let secondPhoto = fetchPhoto(url: "2.jpg")
    async let thirdPhoto = fetchPhoto(url: "3.jpg")
    
    // 直到這一行才會暫停,等待三張都下載完
    let photos = try await [firstPhoto, secondPhoto, thirdPhoto]
    print("三張圖都好了")
}

Task

如果你在同步環境 (例如 SwiftUI 的 Button action 或 UIKit 的 viewDidLoad) 想要呼叫 async 函式,你需要建立一個 Task

// 這是在一般的同步函式中
func onButtonTap() {
    Task {
        // 進入非同步環境
        await showPhoto()
    }
}

Actors

Actor 是一種參照型別 (Reference Type),類似 Class,但它保證同一時間只有一個任務可以存取它的可變狀態。這完美解決了 Data Race (競態條件) 的問題。

定義 Actor 使用 actor 關鍵字:

actor BankAccount {
    var balance: Double = 0.0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Double {
        if balance >= amount {
            balance -= amount
            return amount
        }
        return 0
    }
}

存取 Actor

因為 Actor 會確保執行緒安全,所以從外部存取它的屬性或方法時,必須使用 await (因為如果不巧有別人正在用,你需要等待)。

let account = BankAccount()

Task {
    await account.deposit(amount: 100.0)
    let currentBalance = await account.balance // 讀取屬性也需要 await
    print(currentBalance)
}

MainActor

@MainActor 是一個全域的 Actor,代表主執行緒 (Main Thread)。在 UI 開發中,所有更新 UI 的操作都必須在主執行緒執行。

你可以將類別、屬性或方法標記為 @MainActor,編譯器就會強制確保它們只在主執行緒上執行。

@MainActor
class ViewModel: ObservableObject {
    @Published var name: String = ""
    
    func updateName(newName: String) {
        self.name = newName // 安全地在 Main Thread 更新 UI 相關變數
    }
}

Swift Concurrency 提供了一套現代、安全且高效的工具,徹底改變了 iOS 開發中處理非同步任務的方式。