引言
繼續(xù)學(xué)習(xí)Swift文檔,從上一章節(jié):錯誤處理,我們學(xué)習(xí)了Swift錯誤處理相關(guān)的內(nèi)容,主要有使用throwing函數(shù),throw拋出錯誤、使用do-catch來處理錯誤、將錯誤轉(zhuǎn)為可選值(使用try?)、禁用錯誤傳遞(使用try!)、延遲操作處理(使用defer關(guān)鍵詞)等這些內(nèi)容?,F(xiàn)在,我們學(xué)習(xí)Swift并發(fā)的相關(guān)內(nèi)容。由于篇幅較長,這里分篇來記錄,接下來,F(xiàn)ighting!
Swift 內(nèi)置支持以結(jié)構(gòu)化方式編寫異步和并行代碼。 盡管一次只執(zhí)行一段程序,異步代碼可以掛起并稍后恢復(fù)運行。 掛起和恢復(fù)程序中的代碼可以讓它在短時間內(nèi)繼續(xù)操作,例如更新其 UI,同時繼續(xù)處理長時間運行的操作,例如通過網(wǎng)絡(luò)獲取數(shù)據(jù)或解析文件。 并行代碼意味著多段代碼同時運行——例如,具有四核處理器的計算機可以同時運行四段代碼,每個核執(zhí)行一項任務(wù)。 使用并行和異步代碼同時執(zhí)行多個操作的程序; 它可以將正在等待外部系統(tǒng)的操作掛起,并可以更容易地以內(nèi)存安全的方式編寫此代碼。
并行或異步代碼帶來的額外的靈活性調(diào)度也伴隨著復(fù)雜性增加的代價。 Swift 允許您以某種方式表達您的意圖,從而啟用一些編譯時檢查——例如,您可以使用 actor 來安全地訪問可變狀態(tài)。 但是,向緩慢或有缺陷的代碼添加并發(fā)并不能保證它會變得快速或正確。 事實上,添加并發(fā)甚至可能會使您的代碼更難調(diào)試。 但是,在需要并發(fā)的代碼中使用支持并發(fā)的Swift語言,可以幫助您在編譯時發(fā)現(xiàn)問題。
本章的其余部分使用并發(fā)術(shù)語來指代異步和并行代碼的這種常見組合。
注意
如果您之前編寫過并發(fā)代碼,您可能習(xí)慣于使用線程。 Swift 中的并發(fā)模型建立在線程之上,但您不直接與它們交互。 Swift 中的異步函數(shù)可以放棄它正在運行的線程,這讓另一個異步函數(shù)在該線程上運行而第一個函數(shù)被阻塞。
盡管可以在不使用 Swift 語言的情況下編寫并發(fā)代碼,但該代碼往往更難閱讀。 例如,以下代碼下載照片名稱列表,下載該列表中的第一張照片,并向用戶顯示該照片:
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[1]
downloadPhoto(named: name) { photo in
show(photo)
}
}
即使在這種簡單的情況下,您最終必須編寫嵌套閉包的代碼來完成一系列處理程序。 在這種風(fēng)格中,具有深層嵌套的更復(fù)雜的代碼很快就會變得笨拙。
定義和調(diào)用異步函數(shù)
異步函數(shù)或異步方法是一種特殊的函數(shù)或方法,可以在執(zhí)行過程中掛起。 這與普通的同步函數(shù)和方法形成對比,它們要么運行完成,要么拋出錯誤,要么永不返回。 異步函數(shù)或方法仍然會做這三件事中的一件,但它也可以在等待某事時在中間掛起。 在異步函數(shù)或方法的主體內(nèi),您標(biāo)記每個可以掛起執(zhí)行的位置。
為了表明一個函數(shù)或方法是異步的,你可以在它的參數(shù)后面的聲明中寫一個 async 關(guān)鍵字,類似于你如何使用 throws 來標(biāo)記一個拋出函數(shù)。 如果函數(shù)或方法返回一個值,則在返回箭頭 (->) 之前寫入 async。 例如,以下是獲取圖庫中照片名稱的方法:
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
對于既是異步又是拋出的函數(shù)或方法,可以在throws之前編寫async。
調(diào)用異步方法時,執(zhí)行會掛起,直到該方法返回。 您在調(diào)用前寫入 await 以標(biāo)記可能的暫停點。 這就像在調(diào)用拋出函數(shù)時編寫 try 一樣,如果出現(xiàn)錯誤,則標(biāo)記程序流程可能發(fā)生的變化。 在異步方法中,只有在調(diào)用另一個異步方法時才會掛起執(zhí)行流程——掛起永遠不會是隱式或搶占式的——這意味著每個可能的暫停點都被標(biāo)記為 await。
例如,下面的代碼獲取圖庫中所有圖片的名稱,然后顯示第一張圖片:
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[1]
let photo = await downloadPhoto(named: name)
show(photo)
因為 listPhotos(inGallery:) 和 downloadPhoto(named:) 函數(shù)都需要進行網(wǎng)絡(luò)請求,所以它們可能需要相對較長的時間才能完成。 通過在返回箭頭之前編寫 async 使它們都異步,讓應(yīng)用程序的其余代碼在等待圖片準(zhǔn)備好時繼續(xù)運行。
為了理解上面例子的并發(fā)特性,這里說說大概的執(zhí)行順序:
- 代碼從第一行開始運行,一直運行到第一個 await。 它調(diào)用 listPhotos(inGallery:) 函數(shù)并在等待該函數(shù)返回時暫停執(zhí)行。
- 當(dāng)此代碼的執(zhí)行暫停時,同一程序中的其他一些并發(fā)代碼會運行。 例如,一個長時間運行的后臺任務(wù)可能會繼續(xù)更新新照片畫廊的列表。 該代碼也一直運行到下一個掛起點,由 await 標(biāo)記,或者直到它完成。
- listPhotos(inGallery:) 返回后,此代碼從該點開始繼續(xù)執(zhí)行。 它將返回的值分配給 photoNames。
- 定義 sortedNames 和 name 的這兩行是常見的同步代碼。 因為在這些行上沒有標(biāo)記 await,所以沒有任何的暫停點。
- 下一個 await 標(biāo)記對 downloadPhoto(named:) 函數(shù)的調(diào)用。 此代碼再次暫停執(zhí)行,直到該函數(shù)返回,從而為其他并發(fā)代碼提供運行的機會。
- downloadPhoto(named:)返回后,它的返回值被賦值給photo,然后在調(diào)用show(_:)時作為參數(shù)傳遞。
代碼中標(biāo)有 await 的掛起點表示當(dāng)前代碼段可能會在等待異步函數(shù)或方法返回時暫停執(zhí)行。 這也稱為讓出線程,因為在后臺,Swift 會暫停您在當(dāng)前線程上執(zhí)行的代碼,并在該線程上運行其他一些代碼。 因為帶有 await 的代碼需要能夠暫停執(zhí)行,所以只有程序中的某些地方可以調(diào)用異步函數(shù)或方法:
- 異步函數(shù)、方法或?qū)傩缘闹黧w中的代碼。
- 用@main 標(biāo)記的結(jié)構(gòu)、類或枚舉的靜態(tài) main() 方法中的代碼。
- 分離的子任務(wù)中的代碼,如下面的Unstructured Concurrency
中所示。
注意
Task.sleep(_:) 方法在編寫簡單代碼以了解并發(fā)工作原理時很有用。 此方法什么都不做,但在返回之前至少等待給定的納秒數(shù)。 這是 listPhotos(inGallery:) 函數(shù)的一個版本,它使用 sleep() 來模擬等待網(wǎng)絡(luò)操作:
func listPhotos(inGallery name: String) async -> [String] {
await Task.sleep(2 * 1_000_000_000) // Two seconds
return ["IMG001", "IMG99", "IMG0404"]
}
異步隊列
上一節(jié)中的 listPhotos(inGallery:) 函數(shù)在數(shù)組的所有元素都準(zhǔn)備就緒后一次性異步返回整個數(shù)組。 另一種方法是使用異步隊列一次等待集合的一個元素。 以下是對異步隊列進行迭代的樣子:
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
上面的例子沒有使用普通的 for-in 循環(huán),而是在它后面寫了 for with await。 就像調(diào)用異步函數(shù)或方法時一樣,寫 await 表示可能的暫停點。 for-await-in 循環(huán)可能會在每次迭代開始時暫停執(zhí)行,等待下一個可用的元素。
就像您可以通過添加對 Sequence 協(xié)議的一致性在 for-in 循環(huán)中使用自己的類型一樣,您可以通過添加對 AsyncSequence 協(xié)議的一致性在 for-await-in 循環(huán)中使用自己的類型。
并行調(diào)用異步函數(shù)
使用 await 調(diào)用異步函數(shù)一次只運行一段代碼。 當(dāng)異步代碼運行時,調(diào)用者會等待該代碼完成,然后再繼續(xù)運行下一行代碼。 例如,要從圖庫中獲取前三張照片,您可以等待對 downloadPhoto(named:) 函數(shù)的三個調(diào)用,如下所示:
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
這種方法有一個重要的缺點:雖然下載是異步的,并且在下載過程中允許其他工作發(fā)生,但一次只運行一次對 downloadPhoto(named:) 的調(diào)用。 每張照片都下載完成了才會開始下載下一張照片。 但是,以下操作無需等待——每張照片都可以獨立下載,甚至可以同時下載。
要調(diào)用異步函數(shù)并讓它與其周圍的代碼并行運行,請在定義常量時在 let 前面寫上 async ,然后在每次使用常量時寫上 await 。
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
在此示例中,對 downloadPhoto(named:) 的所有三個調(diào)用都開始而不等待前一個調(diào)用完成。 如果有足夠的系統(tǒng)資源可用,它們可以同時運行。 這些函數(shù)調(diào)用都沒有標(biāo)記為 await,因為代碼不會掛起以等待函數(shù)的結(jié)果。 相反,執(zhí)行會一直持續(xù)到定義照片的那一行——在這一點上,程序需要這些異步調(diào)用的結(jié)果,所以可以編寫 await 來暫停執(zhí)行,直到所有三張照片都完成下載。
以下是您如何思考這兩種方法之間的差異:
- 當(dāng)以下行中的代碼取決于該函數(shù)的結(jié)果時,使用 await 調(diào)用異步函數(shù)。 這將創(chuàng)建按順序執(zhí)行的工作。
- 當(dāng)您在代碼中稍后才需要結(jié)果時,請使用 async-let 調(diào)用異步函數(shù)。 這將創(chuàng)建可以并行執(zhí)行的工作。
- await 和 async-let 都允許其他代碼在掛起時運行。
- 在這兩種情況下,您都用 await 標(biāo)記可能的暫停點以指示執(zhí)行將暫停(如果需要),直到異步函數(shù)返回。
您還可以在同一代碼中混合使用這兩種方法。
任務(wù)和任務(wù)組
任務(wù)是一個工作單元,可以作為程序的一部分異步運行。 所有異步代碼都作為某些任務(wù)的一部分運行。 上一節(jié)中描述的 async-let 語法為您創(chuàng)建了一個子任務(wù)。 您還可以創(chuàng)建一個任務(wù)組并將子任務(wù)添加到該組中,這使您可以更好地控制優(yōu)先級和取消操作,并讓您創(chuàng)建動態(tài)數(shù)量的任務(wù)。
任務(wù)按層次結(jié)構(gòu)排列。 任務(wù)組中的每個任務(wù)都有相同的父任務(wù),每個任務(wù)可以有子任務(wù)。 由于任務(wù)和任務(wù)組之間存在顯式關(guān)系,因此這種方法稱為結(jié)構(gòu)化并發(fā)。 盡管在代碼的正確性無法保證,但任務(wù)之間顯式的父子關(guān)系讓 Swift可以 處理一些行為,例如為您進行取消操作,并讓 Swift 在編譯時檢測到一些錯誤。
await withTaskGroup(of: Data.self) { taskGroup in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
taskGroup.async { await downloadPhoto(named: name) }
}
}
有關(guān)任務(wù)組更多的信息,請移步TaskGroup。
非結(jié)構(gòu)化并發(fā)
除了前面幾節(jié)中描述的結(jié)構(gòu)化并發(fā)方法之外,Swift 還支持非結(jié)構(gòu)化并發(fā)。 與屬于任務(wù)組的任務(wù)不同,非結(jié)構(gòu)化任務(wù)沒有父任務(wù)。 您可以完全靈活地以任何程序需要的方式管理非結(jié)構(gòu)化任務(wù),但您也對它們的正確性完全負責(zé)。 要創(chuàng)建在當(dāng)前 actor 上運行的非結(jié)構(gòu)化任務(wù),請調(diào)用 async(priority:operation:) 函數(shù)。 要創(chuàng)建不屬于當(dāng)前參與者的非結(jié)構(gòu)化任務(wù),更具體地說,稱為分離任務(wù),請調(diào)用 asyncDetached(priority:operation:)。 這兩個函數(shù)都返回一個任務(wù)操作,讓您可以與任務(wù)進行交互——例如,等待其結(jié)果或取消它。
let newPhoto = // ... some photo data ...
let handle = async {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.get()
有關(guān)管理分離任務(wù)的更多信息,請參閱Task.Handle。
任務(wù)取消
Swift 并發(fā)使用協(xié)作取消模型。 每個任務(wù)檢查它是否在其執(zhí)行的適當(dāng)點被取消,并以任何適當(dāng)?shù)姆绞巾憫?yīng)取消。 根據(jù)您所做的工作,這通常意味著以下情況之一:
- 像CancellationError一樣拋出錯誤
- 返回nil或者空的集合
- 退回部分完成的工作
要檢查取消,請調(diào)用 Task.checkCancellation()
,如果任務(wù)被取消,它會拋出 CancellationError,或者檢查 Task.isCancelled
的值并在您自己的代碼中處理取消。 例如,從圖庫下載照片的任務(wù)可能需要刪除部分下載并關(guān)閉網(wǎng)絡(luò)連接。
要手動取消,請調(diào)用Task.Handle.cancel()。
Actors
和類一樣,actor 也是引用類型,所以 Classes Are Reference Types 中值類型和引用類型的比較既適用于 actor,也適用于類。 與類不同,actor 一次只允許一個任務(wù)訪問其可變狀態(tài),這使得多個任務(wù)中的代碼可以安全地與同一個 actor 實例交互。 例如,這是一個記錄溫度的 actor:
actor TemperatureLogger {
let label: String
var measurements: [Int]
private(set) var max: Int
init(label: String, measurement: Int) {
self.label = label
self.measurements = [measurement]
self.max = measurement
}
}
你用 actor 關(guān)鍵字引入一個 actor類,然后用一對大括號來定義它。 TemperatureLogger 角色具有角色外部的其他代碼可以訪問的屬性,并限制了 max 屬性,因此只有角色內(nèi)部的代碼才能更新最大值。
您可以使用與結(jié)構(gòu)體和類相同的初始化器語法來創(chuàng)建 actor 的實例。 當(dāng)你訪問一個 actor 的屬性或方法時,你使用 await 來標(biāo)記潛在的暫停點——例如:
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
在這個例子中,訪問 logger.max 是一個掛起的地方。 因為 actor 一次只允許一個任務(wù)訪問其可變狀態(tài),如果來自另一個任務(wù)的代碼已經(jīng)與logger交互,則該代碼在等待訪問該屬性時掛起。
相比之下,actor 的一部分代碼在訪問 actor 的屬性時不會編寫 await。 例如,這是一個使用新溫度更新 TemperatureLogger 的方法:
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
update(with:) 方法已經(jīng)在 actor 上運行,所以它不會用 await 標(biāo)記它對 max 等屬性的訪問。 此方法還顯示了 Actor 一次只允許一項任務(wù)與其可變狀態(tài)交互的原因之一:對 Actor 狀態(tài)的某些更新會暫時更改不可變量。 TemperatureLogger actor 會跟蹤溫度列表和最高溫度,并在您記錄新測量值時更新最高溫度。 在更新過程中,在添加新測量值之后,但在更新 max 之前,溫度記錄器處于臨時不一致狀態(tài)。 防止多個任務(wù)同時與同一個實例交互可以防止出現(xiàn)類似以下事件隊列的問題:
- 您的代碼調(diào)用 update(with:) 方法。 它首先更新測量數(shù)組。
- 在您的代碼可以更新 max 之前,其他地方的代碼會讀取最大值和溫度數(shù)組。
- 您的代碼通過更改 max 來完成更新。
在這種情況下,在別處運行的代碼會讀取不正確的信息,因為它對 actor 的訪問在調(diào)用 update(with:) 的過程中交錯進行,而數(shù)據(jù)暫時是無效的。 您可以在使用 Swift actor 時防止出現(xiàn)此問題,因為它們一次只允許對其狀態(tài)進行一次操作,并且因為該代碼只能在 await 標(biāo)記暫停點的地方中斷。 因為 update(with:) 不包含任何暫停點,所以沒有其他代碼可以在更新過程中訪問數(shù)據(jù)。
如果您嘗試從 actor 外部訪問這些屬性,就像使用類的實例一樣,您將收到編譯時錯誤; 例如:
print(logger.max) // Error
在不寫入 await 的情況下訪問 logger.max 會失敗,因為 actor 的屬性是該 actor 隔離的本地狀態(tài)的一部分。 Swift 保證只有 Actor 內(nèi)部的代碼才能訪問 Actor 的本地狀態(tài)。 這種保證稱為參與者隔離。
總結(jié)
Swift支持使用異步和并行代碼來處理耗時操作,就像OC中使用GCD或NSOperation編寫多線程一樣;相比之下,Swift語法要簡單的多。
- 使用
async關(guān)鍵字來實現(xiàn)異步操作,寫法有兩種,如下:
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
async let firstPhoto = downloadPhoto(named: photoNames[0])
- 使用
await關(guān)鍵字來實現(xiàn)等待耗時操作完成后,再執(zhí)行后面的操作,相當(dāng)于在一個線程上同步運行任務(wù),如下代碼,只會同步調(diào)用下載任務(wù),這種操作比較耗時。
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
-
async和await配合使用實現(xiàn)多線程操作,并行調(diào)用方法,等待任務(wù)全部完成后,再執(zhí)行之后的任務(wù),如:
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
- 這里簡單的說了一下任務(wù)和任務(wù)組的概念,將任務(wù)加入到任務(wù)組,可以方便的控制任務(wù)優(yōu)先級和進行取消操作,創(chuàng)建動態(tài)的任務(wù)數(shù)量,更多詳細的內(nèi)容,請看
TaskGroup - 任務(wù)處理的相關(guān)操作,如任務(wù)取消,請參閱
Task.Handle。 - 使用
actor關(guān)鍵字來避免數(shù)據(jù)競爭,相當(dāng)于OC中的鎖。
并發(fā)的內(nèi)容就這些了,最后的最后,以上總結(jié)若有錯誤,請指正!喜歡的朋友也麻煩您點個贊喲~
參考文檔:[Swift - Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html