Swift 5.x - 并發(fā)(中文文檔)

引言

繼續(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í)行順序:

  1. 代碼從第一行開始運行,一直運行到第一個 await。 它調(diào)用 listPhotos(inGallery:) 函數(shù)并在等待該函數(shù)返回時暫停執(zhí)行。
  2. 當(dāng)此代碼的執(zhí)行暫停時,同一程序中的其他一些并發(fā)代碼會運行。 例如,一個長時間運行的后臺任務(wù)可能會繼續(xù)更新新照片畫廊的列表。 該代碼也一直運行到下一個掛起點,由 await 標(biāo)記,或者直到它完成。
  3. listPhotos(inGallery:) 返回后,此代碼從該點開始繼續(xù)執(zhí)行。 它將返回的值分配給 photoNames。
  4. 定義 sortedNames 和 name 的這兩行是常見的同步代碼。 因為在這些行上沒有標(biāo)記 await,所以沒有任何的暫停點。
  5. 下一個 await 標(biāo)記對 downloadPhoto(named:) 函數(shù)的調(diào)用。 此代碼再次暫停執(zhí)行,直到該函數(shù)返回,從而為其他并發(fā)代碼提供運行的機會。
  6. 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)
  • asyncawait配合使用實現(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é)若有錯誤,請指正!喜歡的朋友也麻煩您點個贊喲~

上一章節(jié):錯誤處理

參考文檔:[Swift - Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 引言 繼續(xù)學(xué)習(xí)Swift文檔,從上一章節(jié):可選鏈接,我們學(xué)習(xí)了Swift可選鏈接相關(guān)的內(nèi)容。現(xiàn)在,我們學(xué)習(xí)Swif...
    shiyueZ閱讀 1,069評論 0 1
  • 引言 繼續(xù)學(xué)習(xí)Swift文檔,從上一章節(jié):繼承,我們學(xué)習(xí)了Swift繼承相關(guān)的內(nèi)容,如繼承的作用、重寫父類的方法和...
    shiyueZ閱讀 2,268評論 0 2
  • 引言 繼續(xù)學(xué)習(xí)Swift文檔,從上一章節(jié):析構(gòu)函數(shù),我們學(xué)習(xí)了Swift析構(gòu)函數(shù)相關(guān)的內(nèi)容?,F(xiàn)在,我們學(xué)習(xí)Swif...
    shiyueZ閱讀 682評論 0 0
  • 引言 繼續(xù)學(xué)習(xí)Swift文檔,從上一章節(jié):函數(shù),我們學(xué)習(xí)了Swift函數(shù)相關(guān)的內(nèi)容,如函數(shù)的定義和使用、函數(shù)參數(shù)、...
    shiyueZ閱讀 2,011評論 0 1
  • 引言 繼續(xù)學(xué)習(xí)Swift文檔,從上一章節(jié):開篇 ,我們了解Swift基本的知識點,現(xiàn)在我們還是從詳細的基礎(chǔ)知識...
    shiyueZ閱讀 6,340評論 3 8

友情鏈接更多精彩內(nèi)容