引言
繼續(xù)學習Swift文檔碴里,從上一章節(jié):錯誤處理签赃,我們學習了Swift錯誤處理相關(guān)的內(nèi)容哨坪,主要有使用throwing函數(shù),throw拋出錯誤轿腺、使用do-catch來處理錯誤两嘴、將錯誤轉(zhuǎn)為可選值(使用try?)、禁用錯誤傳遞(使用try!)族壳、延遲操作處理(使用defer關(guān)鍵詞)等這些內(nèi)容°颈瑁現(xiàn)在,我們學習Swift并發(fā)的相關(guān)內(nèi)容仿荆。由于篇幅較長贰您,這里分篇來記錄坏平,接下來,F(xiàn)ighting锦亦!
Swift 內(nèi)置支持以結(jié)構(gòu)化方式編寫異步和并行代碼舶替。 盡管一次只執(zhí)行一段程序,異步代碼可以掛起并稍后恢復運行。 掛起和恢復程序中的代碼可以讓它在短時間內(nèi)繼續(xù)操作杠园,例如更新其 UI坎穿,同時繼續(xù)處理長時間運行的操作,例如通過網(wǎng)絡獲取數(shù)據(jù)或解析文件返劲。 并行代碼意味著多段代碼同時運行——例如玲昧,具有四核處理器的計算機可以同時運行四段代碼,每個核執(zhí)行一項任務篮绿。 使用并行和異步代碼同時執(zhí)行多個操作的程序孵延; 它可以將正在等待外部系統(tǒng)的操作掛起,并可以更容易地以內(nèi)存安全的方式編寫此代碼亲配。
并行或異步代碼帶來的額外的靈活性調(diào)度也伴隨著復雜性增加的代價尘应。 Swift 允許您以某種方式表達您的意圖,從而啟用一些編譯時檢查——例如吼虎,您可以使用 actor 來安全地訪問可變狀態(tài)犬钢。 但是,向緩慢或有缺陷的代碼添加并發(fā)并不能保證它會變得快速或正確思灰。 事實上玷犹,添加并發(fā)甚至可能會使您的代碼更難調(diào)試。 但是洒疚,在需要并發(fā)的代碼中使用支持并發(fā)的Swift語言,可以幫助您在編譯時發(fā)現(xiàn)問題歹颓。
本章的其余部分使用并發(fā)術(shù)語來指代異步和并行代碼的這種常見組合。
注意
如果您之前編寫過并發(fā)代碼油湖,您可能習慣于使用線程巍扛。 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)
}
}
即使在這種簡單的情況下,您最終必須編寫嵌套閉包的代碼來完成一系列處理程序蹦误。 在這種風格中劫拢,具有深層嵌套的更復雜的代碼很快就會變得笨拙肉津。
定義和調(diào)用異步函數(shù)
異步函數(shù)或異步方法是一種特殊的函數(shù)或方法,可以在執(zhí)行過程中掛起舱沧。 這與普通的同步函數(shù)和方法形成對比妹沙,它們要么運行完成,要么拋出錯誤熟吏,要么永不返回距糖。 異步函數(shù)或方法仍然會做這三件事中的一件,但它也可以在等待某事時在中間掛起牵寺。 在異步函數(shù)或方法的主體內(nèi)悍引,您標記每個可以掛起執(zhí)行的位置。
為了表明一個函數(shù)或方法是異步的帽氓,你可以在它的參數(shù)后面的聲明中寫一個 async 關(guān)鍵字趣斤,類似于你如何使用 throws 來標記一個拋出函數(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 以標記可能的暫停點。 這就像在調(diào)用拋出函數(shù)時編寫 try 一樣玄渗,如果出現(xiàn)錯誤座菠,則標記程序流程可能發(fā)生的變化。 在異步方法中藤树,只有在調(diào)用另一個異步方法時才會掛起執(zhí)行流程——掛起永遠不會是隱式或搶占式的——這意味著每個可能的暫停點都被標記為 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)絡請求,所以它們可能需要相對較長的時間才能完成微王。 通過在返回箭頭之前編寫 async 使它們都異步屡限,讓應用程序的其余代碼在等待圖片準備好時繼續(xù)運行。
為了理解上面例子的并發(fā)特性炕倘,這里說說大概的執(zhí)行順序:
- 代碼從第一行開始運行钧大,一直運行到第一個 await。 它調(diào)用 listPhotos(inGallery:) 函數(shù)并在等待該函數(shù)返回時暫停執(zhí)行罩旋。
- 當此代碼的執(zhí)行暫停時啊央,同一程序中的其他一些并發(fā)代碼會運行眶诈。 例如,一個長時間運行的后臺任務可能會繼續(xù)更新新照片畫廊的列表瓜饥。 該代碼也一直運行到下一個掛起點逝撬,由 await 標記,或者直到它完成乓土。
- listPhotos(inGallery:) 返回后宪潮,此代碼從該點開始繼續(xù)執(zhí)行。 它將返回的值分配給 photoNames趣苏。
- 定義 sortedNames 和 name 的這兩行是常見的同步代碼狡相。 因為在這些行上沒有標記 await,所以沒有任何的暫停點食磕。
- 下一個 await 標記對 downloadPhoto(named:) 函數(shù)的調(diào)用尽棕。 此代碼再次暫停執(zhí)行,直到該函數(shù)返回芬为,從而為其他并發(fā)代碼提供運行的機會萄金。
- downloadPhoto(named:)返回后,它的返回值被賦值給photo媚朦,然后在調(diào)用show(_:)時作為參數(shù)傳遞氧敢。
代碼中標有 await 的掛起點表示當前代碼段可能會在等待異步函數(shù)或方法返回時暫停執(zhí)行。 這也稱為讓出線程询张,因為在后臺孙乖,Swift 會暫停您在當前線程上執(zhí)行的代碼,并在該線程上運行其他一些代碼份氧。 因為帶有 await 的代碼需要能夠暫停執(zhí)行唯袄,所以只有程序中的某些地方可以調(diào)用異步函數(shù)或方法:
- 異步函數(shù)、方法或?qū)傩缘闹黧w中的代碼蜗帜。
- 用@main 標記的結(jié)構(gòu)恋拷、類或枚舉的靜態(tài) main() 方法中的代碼。
- 分離的子任務中的代碼厅缺,如下面的Unstructured Concurrency
中所示蔬顾。
注意
Task.sleep(_:) 方法在編寫簡單代碼以了解并發(fā)工作原理時很有用。 此方法什么都不做湘捎,但在返回之前至少等待給定的納秒數(shù)诀豁。 這是 listPhotos(inGallery:) 函數(shù)的一個版本,它使用 sleep() 來模擬等待網(wǎng)絡操作:
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ù)組的所有元素都準備就緒后一次性異步返回整個數(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ù)一次只運行一段代碼遇汞。 當異步代碼運行時未妹,調(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)用都沒有標記為 await钉蒲,因為代碼不會掛起以等待函數(shù)的結(jié)果切端。 相反,執(zhí)行會一直持續(xù)到定義照片的那一行——在這一點上顷啼,程序需要這些異步調(diào)用的結(jié)果帆赢,所以可以編寫 await 來暫停執(zhí)行,直到所有三張照片都完成下載线梗。
以下是您如何思考這兩種方法之間的差異:
- 當以下行中的代碼取決于該函數(shù)的結(jié)果時,使用 await 調(diào)用異步函數(shù)怠益。 這將創(chuàng)建按順序執(zhí)行的工作仪搔。
- 當您在代碼中稍后才需要結(jié)果時,請使用 async-let 調(diào)用異步函數(shù)蜻牢。 這將創(chuàng)建可以并行執(zhí)行的工作烤咧。
- await 和 async-let 都允許其他代碼在掛起時運行偏陪。
- 在這兩種情況下,您都用 await 標記可能的暫停點以指示執(zhí)行將暫停(如果需要)煮嫌,直到異步函數(shù)返回笛谦。
您還可以在同一代碼中混合使用這兩種方法。
任務和任務組
任務是一個工作單元昌阿,可以作為程序的一部分異步運行饥脑。 所有異步代碼都作為某些任務的一部分運行。 上一節(jié)中描述的 async-let 語法為您創(chuàng)建了一個子任務懦冰。 您還可以創(chuàng)建一個任務組并將子任務添加到該組中灶轰,這使您可以更好地控制優(yōu)先級和取消操作,并讓您創(chuàng)建動態(tài)數(shù)量的任務刷钢。
任務按層次結(jié)構(gòu)排列笋颤。 任務組中的每個任務都有相同的父任務,每個任務可以有子任務内地。 由于任務和任務組之間存在顯式關(guān)系伴澄,因此這種方法稱為結(jié)構(gòu)化并發(fā)。 盡管在代碼的正確性無法保證阱缓,但任務之間顯式的父子關(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)任務組更多的信息,請移步TaskGroup
清焕。
非結(jié)構(gòu)化并發(fā)
除了前面幾節(jié)中描述的結(jié)構(gòu)化并發(fā)方法之外,Swift 還支持非結(jié)構(gòu)化并發(fā)祭犯。 與屬于任務組的任務不同秸妥,非結(jié)構(gòu)化任務沒有父任務。 您可以完全靈活地以任何程序需要的方式管理非結(jié)構(gòu)化任務沃粗,但您也對它們的正確性完全負責粥惧。 要創(chuàng)建在當前 actor 上運行的非結(jié)構(gòu)化任務,請調(diào)用 async(priority:operation:) 函數(shù)最盅。 要創(chuàng)建不屬于當前參與者的非結(jié)構(gòu)化任務突雪,更具體地說,稱為分離任務涡贱,請調(diào)用 asyncDetached(priority:operation:)咏删。 這兩個函數(shù)都返回一個任務操作,讓您可以與任務進行交互——例如问词,等待其結(jié)果或取消它督函。
let newPhoto = // ... some photo data ...
let handle = async {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.get()
有關(guān)管理分離任務的更多信息,請參閱Task.Handle
。
任務取消
Swift 并發(fā)使用協(xié)作取消模型辰狡。 每個任務檢查它是否在其執(zhí)行的適當點被取消锋叨,并以任何適當?shù)姆绞巾憫∠?根據(jù)您所做的工作,這通常意味著以下情況之一:
- 像CancellationError一樣拋出錯誤
- 返回nil或者空的集合
- 退回部分完成的工作
要檢查取消宛篇,請調(diào)用 Task.checkCancellation()
娃磺,如果任務被取消,它會拋出 CancellationError叫倍,或者檢查 Task.isCancelled
的值并在您自己的代碼中處理取消偷卧。 例如,從圖庫下載照片的任務可能需要刪除部分下載并關(guān)閉網(wǎng)絡連接段标。
要手動取消涯冠,請調(diào)用Task.Handle.cancel()
。
Actors
和類一樣逼庞,actor 也是引用類型蛇更,所以 Classes Are Reference Types 中值類型和引用類型的比較既適用于 actor,也適用于類赛糟。 與類不同派任,actor 一次只允許一個任務訪問其可變狀態(tài),這使得多個任務中的代碼可以安全地與同一個 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 的實例皿伺。 當你訪問一個 actor 的屬性或方法時,你使用 await 來標記潛在的暫停點——例如:
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
在這個例子中盒粮,訪問 logger.max 是一個掛起的地方鸵鸥。 因為 actor 一次只允許一個任務訪問其可變狀態(tài),如果來自另一個任務的代碼已經(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 標記它對 max 等屬性的訪問呢簸。 此方法還顯示了 Actor 一次只允許一項任務與其可變狀態(tài)交互的原因之一:對 Actor 狀態(tài)的某些更新會暫時更改不可變量汁讼。 TemperatureLogger actor 會跟蹤溫度列表和最高溫度淆攻,并在您記錄新測量值時更新最高溫度。 在更新過程中嘿架,在添加新測量值之后,但在更新 max 之前,溫度記錄器處于臨時不一致狀態(tài)啸箫。 防止多個任務同時與同一個實例交互可以防止出現(xiàn)類似以下事件隊列的問題:
- 您的代碼調(diào)用 update(with:) 方法耸彪。 它首先更新測量數(shù)組。
- 在您的代碼可以更新 max 之前忘苛,其他地方的代碼會讀取最大值和溫度數(shù)組蝉娜。
- 您的代碼通過更改 max 來完成更新。
在這種情況下扎唾,在別處運行的代碼會讀取不正確的信息召川,因為它對 actor 的訪問在調(diào)用 update(with:) 的過程中交錯進行,而數(shù)據(jù)暫時是無效的胸遇。 您可以在使用 Swift actor 時防止出現(xiàn)此問題荧呐,因為它們一次只允許對其狀態(tài)進行一次操作,并且因為該代碼只能在 await 標記暫停點的地方中斷纸镊。 因為 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í)行后面的操作期奔,相當于在一個線程上同步運行任務,如下代碼危尿,只會同步調(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)
-
async
和await
配合使用實現(xiàn)多線程操作谊娇,并行調(diào)用方法肺孤,等待任務全部完成后,再執(zhí)行之后的任務,如:
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)
- 這里簡單的說了一下任務和任務組的概念赠堵,將任務加入到任務組小渊,可以方便的控制任務優(yōu)先級和進行取消操作,創(chuàng)建動態(tài)的任務數(shù)量茫叭,更多詳細的內(nèi)容酬屉,請看
TaskGroup
- 任務處理的相關(guān)操作,如任務取消揍愁,請參閱
Task.Handle
呐萨。 - 使用
actor
關(guān)鍵字來避免數(shù)據(jù)競爭,相當于OC中的鎖莽囤。
并發(fā)的內(nèi)容就這些了谬擦,最后的最后,以上總結(jié)若有錯誤朽缎,請指正惨远!喜歡的朋友也麻煩您點個贊喲~
參考文檔:[Swift - Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html