Swift5.5 并發(fā)初探

異步函數(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ú)或一起支持asyncthrows關(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屬性同時是asyncthrows,讀取時必須使用try await:

func printHighScores() async throws {
    let file = BundleFile(filename: "highscores")
    try await print(file.contents)
}

注意點(diǎn):

  • 異步屬性必須是只讀的,可寫屬性不能聲明為異步屬性;
  • 異步屬性需要有一個明確的getterasync關(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中的處理依然是串行的:對loadFromDatabaseawait將使這個異步函數(shù)在此暫停,直到實際操作結(jié)束,接下來才會執(zhí)行loadSignature

task-serial.png

我們當(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)行求值。在上面的例子中,loadFromDatabaseloadSignature將被并發(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)用它,因為它會等到另一個SafeCollector actor能夠處理請求。

需要明確的是,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)用在正確的地方。

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

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

  • 一、async/await的優(yōu)點(diǎn) 1)方便級聯(lián)調(diào)用:即調(diào)用依次發(fā)生的場景; 2)同步代碼編寫方式: Promise...
    puxiaotaoc閱讀 105,911評論 7 62
  • async/await的典型應(yīng)用場景 一、async/await的優(yōu)點(diǎn) 1)方便級聯(lián)調(diào)用 即調(diào)用依次發(fā)生的場景; ...
    西北有高樓lee閱讀 739評論 0 0
  • 相對于回調(diào)函數(shù)來說,Promise是一種相對優(yōu)雅的選擇。那么有沒有更好的方案呢?答案就是async/await。優(yōu)...
    饑人谷_阿銀閱讀 905評論 0 0
  • 相對于回調(diào)函數(shù)來說,Promise是一種相對優(yōu)雅的選擇。那么有沒有更好的方案呢?答案就是async/await。優(yōu)...
    松哥888閱讀 47,870評論 8 36
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,895評論 2 7

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