async-await 是在 WWDC 2021 期間的 Swift 5.5 中的結構化并發(fā)變化的一部分。Swift中的并發(fā)性意味著允許多段代碼同時運行原杂。這是一個非常簡化的描述印颤,但它應該讓你知道 Swift 中的并發(fā)性對你的應用程序的性能是多么重要。有了新的 async 方法和 await 語句穿肄,我們可以定義方法來進行異步工作年局。
你可能讀過Chris Lattner的Swift并發(fā)性宣言Swift Concurrency Manifesto by Chris Lattner,這是在幾年前發(fā)布的咸产。Swift社區(qū)的許多開發(fā)者對未來將出現的定義異步代碼的結構化方式感到興奮∈阜瘢現在它終于來了,我們可以用async-await簡化我們的代碼脑溢,使我們的異步代碼更容易閱讀僵朗。
什么是 async?
async 是異步的意思,可以看作是一個明確表示一個方法是執(zhí)行異步工作的一個屬性屑彻。這樣一個方法的例子看起來如下:
func fetchImages() async throws -> [UIImage] {
// .. 執(zhí)行數據請求
}
fetchImages
方法被定義為異步且可以拋出異常衣迷,這意味著它正在執(zhí)行一個可失敗的異步作業(yè)。如果一切順利酱酬,該方法將返回一組圖像壶谒,如果出現問題,則拋出錯誤膳沽。
async 如何取代完成回調閉包
async 方法取代了經澈共耍看到的完成回調。完成回調在Swift中很常見挑社,用于從異步任務中返回陨界,通常與一個結果類型的參數相結合。上述方法一般會被寫成這樣:
func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
// .. 執(zhí)行數據請求
}
在如今的Swift版本中痛阻,使用完成閉包來定義方法仍然是可行的菌瘪,但它有一些缺點,async 卻剛好可以解決阱当。
- 你必須確保自己在每個可能的退出方法中調用完成閉包俏扩。如果不這樣做,可能會導致應用程序無休止地等待一個結果弊添。
- 閉包代碼比較難閱讀录淡。與結構化并發(fā)相比,對執(zhí)行順序的推理并不那么容易油坝。
- 需要使用弱引用weak references來避免循環(huán)引用嫉戚。
- 實現者需要對結果進行切換以獲得結果刨裆。無法從實現層面使用
try catch
語句。
這些缺點是基于使用相對較新的Result
枚舉的閉包版本彬檀。很可能很多項目仍然在使用完成回調帆啃,而沒有使用這個枚舉:
func fetchImages(completion: ([UIImage]?, Error?) -> Void) {
// .. 執(zhí)行數據請求
}
像這樣定義一個方法使我們很難推理出調用者一方的結果。value
和error
都是可選的窍帝,這要求我們在任何情況下都要進行解包链瓦。對這些可選項解包會導致更多的代碼混亂,這對提高可讀性沒有幫助盯桦。
什么是 await?
await 是用于調用異步方法的關鍵字渤刃。你可以把它們(async-await)看作是Swift中最好的朋友拥峦,因為一個永遠不會離開另一個,你基本上可以這樣說:
"Await 正在等待來自他的伙伴async 的回調"
盡管這聽起來很幼稚卖子,但這并不是騙人的! 我們可以通過調用我們先前定義的異步方法 fetchImages
方法來看一個例子:
do {
let images = try await fetchImages()
print("Fetched \(images.count) images.")
} catch {
print("Fetching images failed with error \(error)")
}
也許你很難相信略号,但上面的代碼例子是在執(zhí)行一個異步任務。使用 await
關鍵字洋闽,我們告訴我們的程序等待 fetchImages
方法的結果玄柠,只有在結果到達后才繼續(xù)。這可能是一個圖像集合诫舅,也可能是一個在獲取圖像時出了什么問題的錯誤羽利。
什么是結構化并發(fā)?
使用 async-await 方法調用的結構化并發(fā)使得執(zhí)行順序的推理更加容易刊懈。方法是線性執(zhí)行的这弧,不用像閉包那樣來回走動。
為了更好地解釋這一點虚汛,我們可以看看在結構化并發(fā)到來之前匾浪,我們如何調用上述代碼示例:
// 1. 調用這個方法
fetchImages { result in
// 3. 異步方法內容返回
switch result {
case .success(let images):
print("Fetched \(images.count) images.")
case .failure(let error):
print("Fetching images failed with error \(error)")
}
}
// 2. 調用方法結束
正如你所看到的,調用方法在獲取圖像之前結束卷哩。最終蛋辈,我們收到了一個結果,然后我們回到了完成回調的流程中将谊。這是一個非結構化的執(zhí)行順序冷溶,可能很難遵循。如果我們在完成回調中執(zhí)行另一個異步方法尊浓,毫無疑問這會增加另一個閉包回調:
// 1. 調用這個方法
fetchImages { result in
// 3. 異步方法內容返回
switch result {
case .success(let images):
print("Fetched \(images.count) images.")
// 4. 調用 resize 方法
resizeImages(images) { result in
// 6. Resize 方法返回
switch result {
case .success(let images):
print("Decoded \(images.count) images.")
case .failure(let error):
print("Decoding images failed with error \(error)")
}
}
// 5. 獲圖片方法返回
case .failure(let error):
print("Fetching images failed with error \(error)")
}
}
// 2. 調用方法結束
每一個閉包都會增加一層縮進挂洛,這使得我們更難理解執(zhí)行的順序。
通過使用 async-await 重寫上述代碼示例眠砾,最好地解釋了結構化并發(fā)的作用虏劲。
do {
// 1. 調用這個方法
let images = try await fetchImages()
// 2.獲圖片方法返回
// 3. 調用 resize 方法
let resizedImages = try await resizeImages(images)
// 4.Resize 方法返回
print("Fetched \(images.count) images.")
} catch {
print("Fetching images failed with error \(error)")
}
// 5. 調用方法結束
執(zhí)行的順序是線性的托酸,因此,容易理解柒巫,容易推理励堡。當我們有時還在執(zhí)行復雜的異步任務時,理解異步代碼會更容易堡掏。
在一個不支持并發(fā)的函數中調用異步方法
在第一次使用 async-awai t時应结,你可能會遇到這樣的錯誤。
當我們試圖從一個不支持并發(fā)的同步調用環(huán)境中調用一個異步方法時泉唁,就會出現這個錯誤鹅龄。我們可以通過將我們的fetchData
方法也定義為異步來解決這個錯誤:
func fetchData() async {
do {
try await fetchImages()
} catch {
// .. handle error
}
}
然而,這將把錯誤轉移到另一個地方亭畜。相反扮休,我們可以使用Task.init
方法,從一個支持并發(fā)的新任務中調用異步方法拴鸵,并將結果分配給我們視圖模型中的一個屬性:
final class ContentViewModel: ObservableObject {
@Published var images: [UIImage] = []
func fetchData() {
Task.init {
do {
self.images = try await fetchImages()
} catch {
// .. handle error
}
}
}
}
使用尾隨閉包的異步方法玷坠,我們創(chuàng)建了一個環(huán)境,在這個環(huán)境中我們可以調用異步方法劲藐。一旦異步方法被調用八堡,獲取數據的方法就會返回,之后所有的異步回調都會在閉包內發(fā)生聘芜。
在一個現有項目中采用 async-await
當在現有項目中采用 async-await 時兄渺,你要注意不要一下子破壞所有的代碼。在進行這樣的大規(guī)模重構時汰现,最好考慮暫時維護舊的實現溶耘,這樣你就不必在知道新的實現是否足夠穩(wěn)定之前更新所有的代碼。這與SDK中被許多不同的開發(fā)者和項目所使用的廢棄方法類似服鹅。
顯然凳兵,你沒有義務這樣做,但它可以使你更容易在你的項目中嘗試使用 async-await企软。除此之外庐扫,Xcode使重構你的代碼變得超級容易,還提供了一個選項來創(chuàng)建一個單獨的 async 方法:
每個重構方法都有自己的目的仗哨,并導致不同的代碼轉換形庭。為了更好地理解其工作原理,我們將使用下面的代碼作為重構的輸入:
struct ImageFetcher {
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
// .. 執(zhí)行數據請求
}
}
將函數轉換為異步 (Convert Function to Async)
第一個重構選項將 fetchImages
方法轉換為異步變量厌漂,而不保留非異步變量萨醒。如果你不想保留原來的實現,這個選項將很有用苇倡。結果代碼如下:
struct ImageFetcher {
func fetchImages() async throws -> [UIImage] {
// .. 執(zhí)行數據請求
}
}
添加異步替代方案 (Add Async Alternative)
添加異步替代重構選項確保保留舊的實現富纸,但會添加一個可用(available) 屬性:
struct ImageFetcher {
@available(*, renamed: "fetchImages()")
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
Task {
do {
let result = try await fetchImages()
completion(.success(result))
} catch {
completion(.failure(error))
}
}
}
func fetchImages() async throws -> [UIImage] {
// .. 執(zhí)行數據請求
}
}
可用屬性對于了解你需要在哪里更新你的代碼以適應新的并發(fā)變量是非常有用的囤踩。雖然,Xcode提供的默認實現并沒有任何警告晓褪,因為它沒有被標記為廢棄的堵漱。要做到這一點,你需要調整可用標記涣仿,如下所示:
@available(*, deprecated, renamed: "fetchImages()")
你可以在我的文章如何在Swift中使用#available屬性中了解更多關于available標記的信息勤庐。
使用這種重構選項的好處是,它允許你逐步適應新的結構化并發(fā)變化好港,而不必一次性轉換你的整個項目榔袋。在這之間進行構建是很有價值的鸣皂,這樣你就可以知道你的代碼變化是按預期工作的之景。利用舊方法的實現將得到如下的警告愿吹。
你可以在整個項目中逐步改變你的實現播急,并使用Xcode中提供的修復按鈕來自動轉換你的代碼以利用新的實現听盖。
添加異步包裝器 (Add Async Wrapper)
最后的重構方法將使用最簡單的轉換杨拐,因為它將簡單地利用你現有的代碼:
struct ImageFetcher {
@available(*, renamed: "fetchImages()")
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
// .. 執(zhí)行數據請求
}
func fetchImages() async throws -> [UIImage] {
return try await withCheckedThrowingContinuation { continuation in
fetchImages() { result in
continuation.resume(with: result)
}
}
}
}
新增加的方法利用了Swift中引入的withCheckedThrowingContinuation
方法棕所,可以不費吹灰之力地轉換基于閉包的方法糊肠。不拋出的方法可以使用withCheckedContinuation
辨宠,其工作原理與此相同,但不支持拋出錯誤货裹。
這兩個方法會暫停當前任務嗤形,直到給定的閉包被調用以觸發(fā) async-await 方法的繼續(xù)。換句話說:你必須確保根據你自己的基于閉包的方法的回調來調用``continuation閉包弧圆。在我們的例子中赋兵,這歸結為用我們從最初的
fetchImages`回調返回的結果值來調用繼續(xù)。
為你的項目選擇正確的 async-await 重構方法
這三個重構選項應該足以將你現有的代碼轉換為異步的替代品搔预。根據你的項目規(guī)模和你的重構時間霹期,你可能想選擇一個不同的重構選項。不過拯田,我強烈建議逐步應用改變历造,因為它允許你隔離改變的部分,使你更容易測試你的改變是否如預期那樣工作船庇。
解決 "Reference to captured parameter ‘self’ in concurrently-executing code "錯誤
在使用異步方法時吭产,另一個常見的錯誤是下面這個:
“Reference to captured parameter ‘self’ in concurrently-executing code”
這大致意思是說我們正試圖引用一個不可變的self
實例。換句話說鸭轮,你可能是在引用一個屬性或一個不可變的實例臣淤,例如,像下面這個例子中的結構體:
不支持從異步執(zhí)行的代碼中修改不可變的屬性或實例窃爷。
可以通過使屬性可變或將結構體更改為引用類型(如類)來修復此錯誤邑蒋。
async-await 將是Result
枚舉的終點嗎姓蜂?
我們已經看到,異步方法取代了利用閉包回調的異步方法寺董。我們可以問自己覆糟,這是否會是Swift中Result
枚舉的終點。最終我們會發(fā)現遮咖,我們真的不再需要它們了滩字,因為我們可以利用try-catch語句與async-await相結合。
Result
枚舉不會很快消失御吞,因為它仍然在整個Swift項目的許多地方被使用麦箍。然而,一旦async-await 的采用率越來越高陶珠,我就不會驚訝地看到它被廢棄挟裂。就我個人而言,除了完成回調揍诽,我沒有在其他地方使用結果枚舉诀蓉。一旦我完全使用 async-await,我就不會再使用這個枚舉了暑脆。
繼續(xù)你的Swift并發(fā)之旅
并發(fā)的變化不僅僅是 async-await渠啤,還包括許多新的功能,你可以從你的代碼中受益√砺穑現在你已經了解了async和await的基礎知識沥曹,現在是時候深入了解其他新的并發(fā)功能了。
- Swift 中的 async/await
- Swift 中的 async let
- Swift 中的 Task
- Swift 中的 Actors 使用以如何及防止數據競爭
- Swift 中的 MainActor 使用和主線程調度
- 理解 Swift Actor 隔離關鍵字:nonisolated 和 isolated
- Swift 中的 Sendable 和 @Sendable 閉包
- Swift 中的 AsyncThrowingStream 和 AsyncStream
- Swift 中的 AsyncSequence
結論
Swift中的 async-await 允許結構化并發(fā)碟联,這將提高復雜異步代碼的可讀性妓美。不再需要完成閉包,而在彼此之后調用多個異步方法的可讀性也大大增強鲤孵。一些新的錯誤類型可能會發(fā)生壶栋,通過確保異步方法是從支持并發(fā)的函數中調用的,同時不改變任何不可變的引用普监,這些錯誤將可以得到解決委刘。