Swift 新并發(fā)框架之 async/await
即使對于經(jīng)驗(yàn)豐富的開發(fā)者來說,寫出健壯性、可維護(hù)性高的并發(fā)代碼也是一項(xiàng)具有挑戰(zhàn)性的任務(wù),其挑戰(zhàn)主要體現(xiàn)在兩個方面:
傳統(tǒng)并發(fā)模型是基于異步模式,代碼維護(hù)性不夠友好; 并發(fā)往往意味著 Data Races,這是一類難復(fù)現(xiàn)、難排查的常見問題。
Swift 在 5.5 開始引入的新并發(fā)框架主要著力解決這 2 個問題。
本文是 『 Swift 新并發(fā)框架 』系列文章的第一篇,主要介紹 Swift 5.5 引入的 async/await。
本系列文章對 Swift 新并發(fā)框架中涉及的內(nèi)容逐個進(jìn)行介紹,內(nèi)容如下:
Swift 新并發(fā)框架之 async/await Swift 新并發(fā)框架之 actor[1] Swift 新并發(fā)框架之 Sendable[2] Swift 新并發(fā)框架之 Task[3]
Overview
在正式開始前,簡單回顧一下同步/異步、串行/并行的概念:
同步(Synchronous)、異步(Asynchronous) 通常指方法(/函數(shù)),同步方法表示直到任務(wù)完成才返回,異步方法則是將任務(wù)拋出去,在任務(wù)完成前就返回;
這也就意味著需要通過某種方式獲得異步任務(wù)的結(jié)果,如:Delegate、Closure 等。
串行(Serial)、并行(Concurrent) 通常指 App 執(zhí)行一組任務(wù)的模式,串行表示一次只能執(zhí)行一個任務(wù),只有當(dāng)前任務(wù)完成后才啟動下一個任務(wù),而并行指可以同時執(zhí)行多個任務(wù)。最常見的莫過于 GCD 中的串行、并行隊(duì)列;
ps. 在此我們不嚴(yán)格區(qū)分并發(fā)、并行的區(qū)別。
傳統(tǒng)的并發(fā)模型都是基于異步模式的,即異步獲取并發(fā)任務(wù)的結(jié)果。
同步代碼是線性的 (straight-line),非常適合人腦處理。
而異步代碼是非線性的、跳躍式的 (類似于 goto 語句),對于單核的人腦來說是一大挑戰(zhàn)。
除了在閱讀上對人腦思維模式構(gòu)成較大挑戰(zhàn)外,異步代碼在具體實(shí)現(xiàn)上常伴有以下問題:
回調(diào)地獄 (Callback Hell); 錯誤處理 (Error Handling); 容易出錯。
初探
我們先通過一個簡單的例子對比一下傳統(tǒng)并發(fā)模型與新的并發(fā)模型間的區(qū)別。
該例子通過 token 獲取頭像,其步驟有:
通過 token 獲取頭像 URL; 通過 URL 下載頭像數(shù)據(jù)(加密); 對頭像數(shù)據(jù)解密; 圖片解碼。
class AvatarLoader {
func loadAvatar(token: String, completion: (Image) -> Void) {
fetchAvatarURL(token: token) { url in
fetchAvatar(url: url) { data in
decryptAvatar(data: data) { data in
decodeImage(data: data) { image in
completion(image)
}
}
}
}
}
func fetchAvatarURL(token: String, completion: (String) -> Void) {
// fetch url from net...
completion(avatarURL)
}
func fetchAvatar(url: String, completion: (Data) -> Void) {
// download avatar data...
completion(avatarData)
}
func decryptAvatar(data: Data, completion: (Data) -> Void) {
// decrypt...
completion(decryptedData)
}
func decodeImage(data: Data, completion: (Image) -> Void) {
// decode...
completion(avatar)
}
}
loadAvatar 方法中回調(diào)層級之深不言而喻。
上述代碼還遺漏了一個重要問題:錯誤處理,其中的網(wǎng)絡(luò)請求、解密、解碼都有可能出錯。
優(yōu)雅地處理錯誤是一項(xiàng)非??简?yàn)基本功的任務(wù)。
一般地,錯誤處理分為 2 種情況:
同步方法:優(yōu)先考慮通過 throw拋出error,這樣調(diào)用方就不得不處理錯誤,因此帶有一定的強(qiáng)制性;異步方法:在回調(diào)中傳遞 error,這種情況下調(diào)用方通常會有意無意地忽略錯誤,使健壯性大打折扣。
為了處理錯誤,對上述代碼進(jìn)行升級:
class AvatarLoader {
func loadAvatar(token: String, completion: (Image?, Error?) -> Void) {
fetchAvatarURL(token: token) { url, error in
guard let url = url else {
// 在這個路徑,經(jīng)常容易漏掉執(zhí)行 completion 或者 return 語句
completion(nil, error)
return
}
fetchAvatar(url: url) { data, error in
guard let data = data else {
completion(nil, error)
return
}
decryptAvatar(data: data) { data, error in
guard let data = data else {
completion(nil, error)
return
}
decodeImage(data: data) { image, error in
completion(image, error)
}
}
}
}
}
func fetchAvatarURL(token: String, completion: (String?, Error?) -> Void) {
// fetch url from net...
completion(avatarURL, error)
}
func fetchAvatar(url: String, completion: (Data?, Error?) -> Void) {
// download avatar data...
completion(avatarData, error)
}
func decryptAvatar(data: Data, completion: (Data?, Error?) -> Void) {
// decrypt...
completion(decryptedData, error)
}
func decodeImage(data: Data, completion: (Image?, Error?) -> Void) {
// decode...
completion(avatar, error)
}
}
可以看到,為了處理錯誤,在 completion 中增加了 error 參數(shù),同時需要將 2 個參數(shù)都定義成 Optional。
同時,在 loadAvatar 中添加了大量的 guard,這樣的代碼無疑非常丑陋。
Optional 無形中增加了代碼成本。
為此,Swift 5 引入了 Result 用于優(yōu)化上述錯誤處理場景:
class AvatarLoader {
func loadAvatar(token: String, completion: (Result<Image, Error>) -> Void) {
fetchAvatarURL(token: token) { result in
switch result {
case let .success(url):
fetchAvatar(url: url) { result in
switch result {
case let .success(decryptData):
decryptAvatar(data: decryptData) { result in
switch result {
case let .success(avaratData):
decodeImage(data: avaratData) { result in
completion(result)
}
case let .failure(error):
completion(.failure(error))
}
}
case let .failure(error):
completion(.failure(error))
}
}
case let .failure(error):
completion(.failure(error))
}
}
}
func fetchAvatarURL(token: String, completion: (Result<String, Error>) -> Void) {
// fetch url from net...
completion(.success(avatarURL))
}
func fetchAvatar(url: String, completion: (Result<Data, Error>) -> Void) {
// download avatar data...
completion(.success(avatarData))
}
func decryptAvatar(data: Data, completion: (Result<Data, Error>) -> Void) {
// decrypt...
completion(.success(decryptData))
}
func decodeImage(data: Data, completion: (Result<Image, Error>) -> Void) {
// decode...
completion(.success(avatar))
}
}
Result 是 enum 類型,含有
success、failure2 個 case。
可以看到,通過使用 Result,參數(shù)不必是 Optional,另外可以通過 switch/case 來處理結(jié)果,在一定程度保證了調(diào)用方對錯誤的處理。
在上面這個 Callback Hell 中,直觀上, Result 不但沒有使代碼簡潔,反而更加復(fù)雜了。
主要是沒有把代碼抽離開來,不要對 Result 有什么誤解^__^。
通過這個簡單的例子,可以看到基于 Callback 的異步模型問題不少。
因此,將異步代碼同步化一直是業(yè)界努力的方向。
如:Promise[4],不過其同步也是建立在 callback 基礎(chǔ)上的。
Swift 5.5 引入了 async/await 用于將異步代碼同步化。
很多語言都已支持
async/await,如:JavaScript、Dart 等
先直觀感受一下 async/await:
class AvatarLoader {
func loadAvatar(token: String) async throws -> Image {
let url = try await fetchAvatarURL(token: token)
let encryptedData = try await fetchAvatar(url: url)
let decryptedData = try await decryptAvatar(data: encryptedData)
return try await decodeImage(data: decryptedData)
}
func fetchAvatarURL(token: String) async throws -> String {
// fetch url from net...
return avatarURL
}
func fetchAvatar(url: String) async throws -> Data {
// download avatar data...
return avatarData
}
func decryptAvatar(data: Data) async throws -> Data {
// decrypt...
return decryptData
}
func decodeImage(data: Data) async throws -> Image {
// decode...
return avatar
}
}
相比基于 Callback 的異步版本,基于 async/await 的版本是不是清晰多了。
尤其是 loadAvatar 方法從感觀上就是一個同步方法,閱讀起來無比順暢。
其錯誤處理也使用了同步式的 throws。
至此,通過對比,對 async/await 有了一個較直觀的認(rèn)識,下面簡單探討一下其實(shí)現(xiàn)機(jī)制。
深究
首先,還是有必要對 async/await 作一個正式的介紹:
async— 用于修飾方法,被修飾的方法則被稱為異步方法 (asynchronous method),異步方法意味著其在執(zhí)行過程中可能會被暫停 (掛起);await— 對 asynchronous method 的調(diào)用需加上await。同時,await只能出現(xiàn)在異步上下文中 (asynchronous context);await則表示一個潛在暫停點(diǎn) (potential suspension points)。
什么是 asynchronous context ?其存在于 2 種環(huán)境下:
asynchronous method body — 異步方法體屬于異步上下文的范疇;
Task closure — Task 任務(wù)閉包也屬于 asynchronous context。
Task 是在 Swift 5.5 中引入的,主要用于創(chuàng)建、執(zhí)行異步任務(wù)。
因此,只能在異步方法或 Task 閉包中通過 await 調(diào)用異步方法。
異步方法執(zhí)行過程中可能會暫停?
potential suspension points?
怎么暫停?
剛開始接觸 async/await 時,下意識地可能會有這些疑問。
2個關(guān)鍵點(diǎn):
暫停的是方法,而不是執(zhí)行方法的線程; 暫停點(diǎn)前后可能會發(fā)生線程切換。
在 Swift 新并發(fā)模型中進(jìn)一步弱化了『 線程 』,理想情況下整個 App 的線程數(shù)應(yīng)與內(nèi)核數(shù)一致,線程的創(chuàng)建、管理完全交由并發(fā)框架負(fù)責(zé)。
Swift 對異步方法 (asynchronous method) 的處理就遵守了上述思想:
異步方法被暫停點(diǎn) (suspension points) 分割為若干個 Job;在并發(fā)框架中 Job是任務(wù)調(diào)度的基本單元;并發(fā)框架根據(jù)實(shí)時情況動態(tài)決定某個 Job的執(zhí)行線程;也就是同一個異步方法中的不同 Job可能運(yùn)行在不同線程上。
正是由于異步方法在其暫停點(diǎn)前后可能會變換執(zhí)行線程,因此在異步方法中要慎用鎖、信號量等同步操作。
let lock = NSLock.init()
func test() async {
lock.lock()
try? await Task.sleep(nanoseconds: 1_000_000_000)
lock.unlock()
}
for i in 0..<10 {
Task {
await test()
}
}
像上面這樣的代碼在 lock.lock() 處會產(chǎn)生死鎖,換成信號量也是一樣。
await 之所以稱為『 潛在 』暫停點(diǎn),而不是暫停點(diǎn),是因?yàn)椴⒉皇撬械?await 都會暫停,只有遇到類似 IO、手動起子線程等情況時才會暫停當(dāng)前調(diào)用棧的運(yùn)行。
總之,對于異步方法如何切分 Job 等細(xì)節(jié)可以不關(guān)心,但 await 可能會暫停當(dāng)前方法的運(yùn)行,并在時機(jī)成熟后在其他線程恢復(fù)運(yùn)行是我們需要明確了解的。
參考資料
swift-evolution/0296-async-await.md at main · apple/swift-evolution · GitHub[5]
swift-evolution/0302-concurrent-value-and-concurrent-closures.md at main · apple/swift-evolution · GitHub[6]
swift-evolution/0337-support-incremental-migration-to-concurrency-checking.md at main · apple/swift-evolution · GitHub[7]
swift-evolution/0304-structured-concurrency.md at main · apple/swift-evolution · GitHub[8]
swift-evolution/0306-actors.md at main · apple/swift-evolution · GitHub[9]
swift-evolution/0337-support-incremental-migration-to-concurrency-checking.md at main · apple/swift-evolution · GitHub[10]
Understanding async/await in Swift ? Andy Ibanez[11]
Concurrency — The Swift Programming Language (Swift 5.6)[12]
Connecting async/await to other Swift code | Swift by Sundell[13]
參考資料
Swift 新并發(fā)框架之 actor: https://juejin.cn/post/7076738494869012494
[2]Swift 新并發(fā)框架之 Sendable: https://juejin.cn/post/7076741945820872717
[3]Swift 新并發(fā)框架之 Task: https://juejin.cn/post/7084640887250092062/
[4]Promise: https://www.promisejs.org/
[5]swift-evolution/0296-async-await.md at main · apple/swift-evolution · GitHub: https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md
[6]swift-evolution/0302-concurrent-value-and-concurrent-closures.md at main · apple/swift-evolution · GitHub: https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md
[7]swift-evolution/0337-support-incremental-migration-to-concurrency-checking.md at main · apple/swift-evolution · GitHub: https://github.com/apple/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md
[8]swift-evolution/0304-structured-concurrency.md at main · apple/swift-evolution · GitHub: https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md#jobs
[9]swift-evolution/0306-actors.md at main · apple/swift-evolution · GitHub: https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md
[10]swift-evolution/0337-support-incremental-migration-to-concurrency-checking.md at main · apple/swift-evolution · GitHub: https://github.com/apple/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md
[11]Understanding async/await in Swift ? Andy Ibanez: https://www.andyibanez.com/posts/understanding-async-await-in-swift/
[12]Concurrency — The Swift Programming Language (Swift 5.6): https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#
[13]Connecting async/await to other Swift code | Swift by Sundell: https://www.swiftbysundell.com/articles/connecting-async-await-with-other-swift-code/
作者:峰之巔
https://juejin.cn/post/7076733264798416926
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取