【開發筆記】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() {}
的修飾,可以透過有限的調整快速實現非同步併發,應用程式的反應性更好,讓用戶在操作上有更舒適的感受。