異步函數(shù)
Swift has built-in support for writing asynchronous and parallel code in a structured way. … the term concurrency to refer to this common combination of asynchronous and parallel code.
???????Swift官方文檔是這樣描述Swift并發(fā)的,它指的就是異步和并行代碼的組合。并行編程需要解決的主要問題:
- 如何確保不同運(yùn)算運(yùn)行步驟間的交互或是通信按照正確的順序執(zhí)行
- 如何確保運(yùn)算資源在不同運(yùn)算之間被安全地共享和訪問
為了更容易和更優(yōu)雅的解決上面兩個問題,在Swift5.5中,引入了異步函數(shù)的概念。在函數(shù)聲明的返回箭頭前面,加上async關(guān)鍵字,就可以把一個函數(shù)聲明為異步函數(shù):
func loadSignature() async -> String {
fatalError("暫未實現(xiàn)")
}
async關(guān)鍵字會幫助編譯器做兩件事情:
- 它允許我們在函數(shù)體內(nèi)部使用
await關(guān)鍵字; - 它要求其他人在調(diào)用這個函數(shù)時,使用
await關(guān)鍵字。
代碼舉例
需求:從服務(wù)器拉取100000條天氣數(shù)據(jù),求取這些數(shù)據(jù)的平均值,然后將平均值回傳給服務(wù)器。
分析:請求服務(wù)器的操作都是異步的毋庸置疑,由于數(shù)據(jù)量過大,求取平均值是個耗時操作,也應(yīng)該異步處理。
常規(guī)代碼實現(xiàn):
func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) {
// 用隨機(jī)值來取代網(wǎng)絡(luò)請求返回的數(shù)據(jù)
DispatchQueue.global().async {
let results = (1...100_000).map { _ in Double.random(in: -10...30) }
completion(results)
}
}
func calculateAverageTemperature(for records: [Double], completion: @escaping (Double) -> Void) {
// 先求和再計算平均值
DispatchQueue.global().async {
let total = records.reduce(0, +)
let average = total / Double(records.count)
completion(average)
}
}
func upload(result: Double, completion: @escaping (String) -> Void) {
// 省略上傳的網(wǎng)絡(luò)請求代碼,均返回"OK"
DispatchQueue.global().async {
completion("OK")
}
}
調(diào)用實現(xiàn)
fetchWeatherHistory { [weak self] records in
self?.calculateAverageTemperature(for: records) { average in
self?.upload(result: average) { response in
print("Server response: \(response)")
}
}
}
存在的問題:
- 可能存在方法中多次調(diào)用或者忘記調(diào)用
completion的情況; - 閉包參數(shù)
@escaping (String) -> Void難以閱讀; - 層層嵌套的回調(diào)代碼看起來很晦澀(所謂的回調(diào)地獄);
- 在swift5.0添加
Result類型之前,使用completion handlers返回錯誤很困難;
async/await實現(xiàn)代碼
func fetchWeatherHistory() async -> [Double] {
(1...100_000).map { _ in Double.random(in: -10...30) }
}
func calculateAverageTemperature(for records: [Double]) async -> Double {
let total = records.reduce(0, +)
let average = total / Double(records.count)
return average
}
func upload(result: Double) async -> String {
"OK"
}
調(diào)用實現(xiàn)
func processWeather() async {
let records = await fetchWeatherHistory()
let average = await calculateAverageTemperature(for: records)
let response = await upload(result: average)
print("Server response: \(response)")
}
僅僅通過async關(guān)鍵字將函數(shù)標(biāo)記為異步返回值,在調(diào)用函數(shù)前加上await關(guān)鍵字,讓整個調(diào)用過程變得簡單清晰,就像在編寫同步代碼一樣。
調(diào)用流程對比

普通函數(shù)的調(diào)用流程:(如上圖)
- 調(diào)用函數(shù);
- 函數(shù)獲取線程的控制權(quán),并完全占有該線程;
- 函數(shù)執(zhí)行完成返回或者拋出錯誤,將控制權(quán)交還調(diào)用方;
這里普通函數(shù)放棄線程控制權(quán)的唯一方式就是執(zhí)行完成。

異步函數(shù)的調(diào)用流程:(如上圖)
- 調(diào)用函數(shù);
- 函數(shù)獲得線程控制權(quán);
- 函數(shù)運(yùn)行后,掛起,同時放棄對線程的控制,并將控制權(quán)交給系統(tǒng),系統(tǒng)可自由支配該線程;
- 系統(tǒng)確定何時恢復(fù)函數(shù);
- 函數(shù)恢復(fù)后重新獲得控制權(quán),并繼續(xù)工作;
- 函數(shù)執(zhí)行完成或拋出異常后,返回調(diào)用方,將控制權(quán)交還給調(diào)用方;
這里需要注意幾點(diǎn):
- 一個異步函數(shù)掛起時,也會掛起它的調(diào)用者,所以調(diào)用者也必須是異步的;
- 異步函數(shù)可以多次掛起;
- 異步函數(shù)掛起時,不會阻塞線程;
- 異步函數(shù)可能會在一個完全不同的線程上恢復(fù);
- async 函數(shù)并不一定會掛起;
異步屬性
???????在Swift5.5中,升級了只讀屬性,以單獨(dú)或一起支持async和throws關(guān)鍵字,使它們更靈活。
enum FileError: Error {
case missing, unreadable
}
struct BundleFile {
let filename: String
var contents: String {
get async throws {
guard let url = Bundle.main.url(forResource: filename, withExtension: nil) else {
throw FileError.missing
}
do {
return try String(contentsOf: url)
} catch {
throw FileError.unreadable
}
}
}
}
因為contents屬性同時是async和throws,讀取時必須使用try await:
func printHighScores() async throws {
let file = BundleFile(filename: "highscores")
try await print(file.contents)
}
注意點(diǎn):
- 異步屬性必須是只讀的,可寫屬性不能聲明為異步屬性;
- 異步屬性需要有一個明確的
getter,async關(guān)鍵字位于get后; - 從Swift 5.5 開始,
getter也可以拋出異常,如果同時是異步的,則async關(guān)鍵字位于throws前面; -
await可用于屬性body中的表達(dá)式,以表明操作的異步性;
結(jié)構(gòu)化并發(fā)
對于同步函數(shù)來說,線程決定了它的執(zhí)行環(huán)境。而對于異步函數(shù),則由任務(wù)(Task)決定執(zhí)行環(huán)境。Swift提供了一系列Task相關(guān)API來讓開發(fā)者創(chuàng)建、組織、檢查和取消任務(wù)。這些API圍繞著Task這一核心類型,為每一組并發(fā)任務(wù)構(gòu)建出一棵結(jié)構(gòu)化的任務(wù)樹:
- 一個任務(wù)具有它自己的優(yōu)先級和取消標(biāo)識,它可以擁有若干個子任務(wù)并在其中執(zhí)行異步函數(shù)。
- 當(dāng)一個父任務(wù)被取消時,這個父任務(wù)的取消標(biāo)識將被設(shè)置,并向下傳遞到所有的子任務(wù)中去。
- 無論是正常完成還是拋出錯誤,子任務(wù)會將結(jié)果向上報告給父任務(wù),在所有子任務(wù)正常完成之前或者有子任務(wù)拋出之前,父任務(wù)是不會被完成的。
這些特性看上去和Operation類有一些相似,不過Task直接利用異步函數(shù)的語法,可以用更簡潔的方式進(jìn)行表達(dá)。而Operation則需要依靠子類或者閉包。
在調(diào)用異步函數(shù)時,需要在它前面添加await關(guān)鍵字;而另一方面,只有在異步函數(shù)中,我們才能使用 await關(guān)鍵字。那么問題在于,第一個異步函數(shù)執(zhí)行的上下文,或者說任務(wù)樹的根節(jié)點(diǎn),是怎么來的?
簡單地使用Task.init就可以讓我們獲取一個任務(wù)執(zhí)行的上下文環(huán)境,它接受一個async標(biāo)記的閉包:
struct Task<Success, Failure> where Failure : Error {
init(
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async throws -> Success
)
}
它繼承當(dāng)前任務(wù)上下文的優(yōu)先級等特性,創(chuàng)建一個新的任務(wù)樹根節(jié)點(diǎn),我們可以在其中使用異步函數(shù):
var results: [String] = []
func someSyncMethod() {
Task {
try await processFromScratch()
print("Done: \(results)")
}
}
func processFromScratch() async throws {
let strings = await loadFromDatabase()
if let signature = try await loadSignature() {
strings.forEach {
results.append($0.appending(signature))
}
} else {
//throw error
}
}
processFromScratch中的處理依然是串行的:對loadFromDatabase的await將使這個異步函數(shù)在此暫停,直到實際操作結(jié)束,接下來才會執(zhí)行loadSignature:

我們當(dāng)然會希望這兩個操作可以同時進(jìn)行,同時,只有當(dāng)兩者都準(zhǔn)備好后,才能調(diào)用appending來實際將簽名附加到數(shù)據(jù)上。這需要任務(wù)以結(jié)構(gòu)化的方式進(jìn)行組織。使用async let綁定可以做到這一點(diǎn):
func processFromScratchNew() async throws {//結(jié)構(gòu)化并發(fā)
async let loadStrings = loadFromDatabase()
async let loadSignature = loadSignature()
let strings = await loadStrings
if let signature = try await loadSignature {
strings.forEach {
results.append($0.appending(signature))
}
} else {
//throw error
}
}
async let被稱為異步綁定,它在當(dāng)前Task上下文中創(chuàng)建新的子任務(wù),并將它用作被綁定的異步函數(shù)的運(yùn)行環(huán)境。和Task.init新建一個任務(wù)根節(jié)點(diǎn)不同,async let所創(chuàng)建的子任務(wù)是任務(wù)樹上的葉子節(jié)點(diǎn),它是結(jié)構(gòu)化的。被異步綁定的操作會立即開始執(zhí)行,即使在await之前執(zhí)行就已經(jīng)完成,其結(jié)果依然可以等到 await語句時再進(jìn)行求值。在上面的例子中,loadFromDatabase和loadSignature將被并發(fā)執(zhí)行。

除了async let外,另一種創(chuàng)建結(jié)構(gòu)化并發(fā)的方式,是使用任務(wù)組(Task group)。比如,我們希望在執(zhí)行 loadResultRemotely的同時,讓processFromScratch一起運(yùn)行,可以將兩個操作寫在同一個task group中:
func someSyncMethod() {
Task {
await withThrowingTaskGroup(of: Void.self) { group in
group.async {
try await self.loadResultRemotely()
}
group.async {
try await self.processFromScratch()
}
}
print("Done: \(results)")
}
}
演員模型
Swift5.5引入了actor,在概念上類似于在并發(fā)環(huán)境中可以安全使用的類,即需要確保在任何時間只能由單個線程訪問actor內(nèi)的可變狀態(tài)。
代碼演示:創(chuàng)建一個RiskyCollector類,該類能夠?qū)崿F(xiàn)兩個收集器對象之間交換牌組中的卡片。
class RiskyCollector {
var deck: Set<String>
init(deck: Set<String>) {
self.deck = deck
}
func send(card selected: String, to person: RiskyCollector) -> Bool {
guard deck.contains(selected) else { return false }
deck.remove(selected)
person.transfer(card: selected)
return true
}
func transfer(card: String) {
deck.insert(card)
}
}
在單線程中,代碼是安全的,但是在多線程中就不安全了,如果我們同時調(diào)用send(card:to:)多次,可能會發(fā)生以下事件鏈:
- 第一個線程檢查卡片是否在牌組中,并且是這樣繼續(xù)。
- 第二個線程還檢查卡片是否在牌組中,并且是這樣繼續(xù)。
- 第一個線程從牌組中取出卡片并將其轉(zhuǎn)移給另一個人。
- 第二個線程試圖從牌組中取出這張牌,但實際上它已經(jīng)消失了,所以什么也不會發(fā)生。但是,它仍然將卡轉(zhuǎn)讓給其他人。
在這種情況下,一個玩家失去1張牌,而另一個玩家得到2張牌,這顯然是不合理的。
通過actor模型可以解決這個問題:除非異步執(zhí)行,否則無法從Actor對象外部讀取存儲的屬性和方法,并且根本無法從 Actor 對象外部寫入存儲的屬性。異步行為不是為了性能;相反,這是因為Swift會自動將這些請求放入一個按順序處理的隊列中,以避免出現(xiàn)競爭條件。因此,我們可以將RiskyCollector類重寫為SafeCollectoractor,如下所示:
actor SafeCollector {
var deck: Set<String>
init(deck: Set<String>) {
self.deck = deck
}
func send(card selected: String, to person: SafeCollector) async -> Bool {
guard deck.contains(selected) else { return false }
deck.remove(selected)
await person.transfer(card: selected)
return true
}
func transfer(card: String) {
deck.insert(card)
}
}
注意點(diǎn):
- Actor是使用
actor關(guān)鍵字創(chuàng)建的。這是Swift中一種新的具體名義類型,用于連接結(jié)構(gòu)體、類和枚舉。 - 該
send()方法標(biāo)有async,因為它需要在等待傳輸完成時暫停其工作。 - 雖然該
transfer(card:)方法沒有用標(biāo)記async,但我們?nèi)匀恍枰?code>await來調(diào)用它,因為它會等到另一個SafeCollectoractor能夠處理請求。
需要明確的是,actor可以自由地、異步或以其他方式使用自己的屬性和方法,但是當(dāng)與不同的actor交互時,它必須始終異步完成。通過這些更改,Swift可以確保永遠(yuǎn)不會同時訪問所有與actor隔離的狀態(tài),更重要的是,這是在編譯時完成的,以保證安全。
Actor和類的對比,相同點(diǎn):
- 兩者都是引用類型,因此它們可用于共享狀態(tài)。
- 它們可以有方法、屬性、初始值設(shè)定項和下標(biāo)。
- 它們可以符合協(xié)議并且是通用的。
- 任何靜態(tài)屬性和方法在這兩種類型中的行為都相同,因為它們沒有
self的概念,因此不會被隔離。
區(qū)別:
- Actors 目前不支持繼承。
- Actors 遵循新的
Actor協(xié)議。
總結(jié)
Swift并發(fā)的概念很多,但是各種的模塊邊界是清晰的:
- 異步函數(shù):提供語法工具,使用更簡潔和高效的方式,表達(dá)異步行為。
- 結(jié)構(gòu)化并發(fā):提供并發(fā)的運(yùn)行環(huán)境,負(fù)責(zé)高效的異步函數(shù)調(diào)度、取消和執(zhí)行順序。
- 演員模型:提供封裝良好的數(shù)據(jù)隔離,確保并發(fā)代碼的安全。
熟悉這些邊界,有助于我們清晰地理解 Swift 并發(fā)各個部分的設(shè)計意圖,從而讓我們手中的工具可以被運(yùn)用在正確的地方。