【开发笔记】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() {}
的修饰,可以透过有限的调整快速实现非同步并发,应用程式的反应性更好,让用户在操作上有更舒适的感受。