首頁
文章
隱私
  • 繁體中文
  • 简体中文
首頁
文章
隱私
  • 繁體中文
  • 简体中文

【開發筆記】Swift Async/Await 併發(Concurrency)實作

併發(Concurrency)在單核心 CPU 上,是透過切換的方式,產生多個任務貌似同時執行的錯覺。而並行(Parallel)則是在併發的基礎上,將任務的執行分配到多 CPU 環境中的不同核心。因為併發通常是多任務在同一個線程(Thread)上運作,因此執行環境的資源是競爭性的共享。而並行由於是分配到不同 CPU 上的不同線程,因此各自擁有自己任務運行的資源。

GCD(Grand Central Dispatch)到 Swift Concurrency

在 Objective-C 還是作為 iOS/macOS 主要開發語言的時期,多執行緒的引用主要是依賴 GCD(Grand Central Dispatch)來實現。甚至 Swift 語言開始導入 iOS/macOS 的開發,很長的一段時間 GCD 依然是實現非同步(asynchronous)功能發配到線程中執行的主要手段。直到 2021 WWDC 發表了 Swift 5.5,這才首次有了自己全新且原生的非同步(asynchronous)/併發模式導入其中。特色主要在於讓開發者能夠以更直觀且避免出錯的方式寫出非同步/併發的程式碼。

async 修飾標記非同步(asynchronous)功能

除了網路遠端調用本身架構的因素,自然是非同步(asynchronous)的操作外,本地端 CPU 密集(CPU intensive)運行的演算也可以作為非同步化的例子來實現。我們可以先從質數的尋找,來模擬 CPU 密集運行的情況。以下是類別方法實作了 number 屬性私有化,value 屬性存取唯讀作為例子,示範 isPrime 功能如何從按序執行轉變成非同步執行的過程。

class MyNumber {
    private let number: Int
    
    init(number: Int) {
        self.number = number
    }
    
    var value: Int {
        get {
            return self.number
        }
    }
    
    var isPrime: Bool {
        get {
            return (1...self.number).filter({ self.number % $0 == 0}).count == 2
        }
    }
        
    var description: String {
        get {
            return self.isPrime ?
            String(format: "\(self.number) (hashValue: \(self.hashValue)) is a prime number.") :
            String(format: "\(self.number) (hashValue: \(self.hashValue)) is not a prime number.")
        }
    }
}

透過 MyNumber 類別,我們可以簡單的調用 isPrime 功能,並且模擬 CPU 密集運行的狀況。

let start = Date()
let aNumber = MyNumber(number: 238939)
print(aNumber.description)
let bNumber = MyNumber(number: 238941)
print(bNumber.description)
let cNumber = MyNumber(number: 238943)
print(cNumber.description)
print(String(format: "Total Time Cost: %.2f sec.", Date().timeIntervalSince(start)))

接著,我們將 isPrime 相關的部分加上 async 敘述,標記出 CPU 密集運行時需要非同步執行的部分。

   ...
   var isPrime: Bool {
        get async {
            return await self.filterAndDivider()
        }
    }
    
    private func filterAndDivider() async -> Bool {
        return (1...self.number).filter({ self.number % $0 == 0}).count == 2
    }
    ...

這時,我們會發現原來的 aNumber.description 無法通過編譯器的檢覈。而修正的方式也很簡單,就是在非同步執行的句子前方加上 await,並且放入 Task {} 中執行。這時候程式碼看起來會變成像下方這樣。

let aNumber = MyNumber(number: 238939)
let bNumber = MyNumber(number: 238941)
let cNumber = MyNumber(number: 238943)
Task {
    let start = Date()
    print(await aNumber.description)
    print(await bNumber.description)
    print(await cNumber.description)
    print(String(format: "Total Time Cost: %.2f sec.", Date().timeIntervalSince(start)))
}

讓編譯器更有效的優化排程

不過,儘管將非同步功能置入 Task {} 中運行,但是透過 await 修飾的句子,依然是由上而下 aNumber -> bNumber -> cNumber 按序執行,而且 aNumber 執行完才會再進行下一個。

如果要讓多個執行任務同時啟動,並且讓編譯器更有效的按照需要來排程,派發執行任務的方式要再進一步的修改,引入 withTaskGroup() {} 讓任務的啟動按照 priority 來排程,並且待所有任務都執行完之後,合併會傳的結果集中輸出。修改過後的程式碼會看起來如下。

Task {
    let start = Date()
    let result = await withTaskGroup(of: String.self, returning: [String].self) { group in
        group.addTask(priority: .low) {
            return await aNumber.description
        }
        group.addTask(priority: .background) {
            return await bNumber.description
        }
        group.addTask(priority: .high) {
            return await cNumber.description
        }
        return await group.reduce(into: [String]()) { result, string in
            result.append(string)
        }
    }
    print(result)
    print(String(format: "Total Time Cost: %.2f sec.", Date().timeIntervalSince(start)))
}

更清楚的結構

相比 GCD,Swift Concurrency 在撰寫上讓程式碼更直觀而且容易閱讀,只要加上一點變化,藉著 async/await,Task {} 以及 withTaskGroup() {} 的修飾,可以透過有限的調整快速實現非同步併發,應用程式的反應性更好,讓用戶在操作上有更舒適的感受。