WWDC21 Swift concurrency: Behind the scenes

簡介

深刻了解Swift併發性的細節,發現Swift如何在提升性能的同時提供更大的安全性,避免數據競爭和線程爆炸。咱們將探討Swift任務與Grand Central Dispatch有何不一樣,新的合做線程模型如何工做,以及如何確保你的應用程序得到最佳性能。爲了得到本次會議的最大收穫,咱們建議首先觀看 "Meet async/await in Swift"、"Explore structured concurrency in Swift"和 "Protect mutable state with Swift actors"。數據庫

前言

wwdc2021-10254_hd-0088.png

今天,咱們和大家談談有關Swift併發的一些基本細微差異。這是一個高級講座,它創建在早期關於Swift併發性的一些講座之上。若是你不熟悉async/await、結構化併發和Actor的概念,我鼓勵你先看一下其餘的講座。在以前關於Swift併發性的講座中,你已經瞭解了今年Swift原生的各類語言特性以及如何使用它們。在此次講座中,咱們將深刻了解爲何這些基元是這樣設計的,不只是爲了語言安全,也是爲了性能和效率。當你在本身的應用中嘗試和採用Swift併發時,咱們但願此次講座能給你一個更好的心理模型,讓你瞭解如何推理Swift併發,以及它如何與Grand Central Dispatch等現有線程庫對接。數組

咱們今天要討論幾件事。緩存

首先,咱們將討論 Swift 併發背後的線程模型,並將其與 Grand Central Dispatch 進行對比。咱們將談論咱們如何利用併發語言的特性,爲Swift創建一個新的線程池,從而實現更好的性能和效率。安全

最後,在這一節中,咱們將談談在將代碼移植到使用Swift併發性時須要注意的事項。markdown

而後,將談論Swift併發中經過Actor進行的同步。 咱們將討論Actor是如何工做的,它們與你可能已經熟悉的現有同步基元(如串行調度隊列)相好比何,網絡

最後,在使用行爲體編寫代碼時須要注意的一些事項。數據結構

今天咱們有不少內容要講,因此讓咱們直接開始吧。多線程

Threading model

Grand Central Dispatch

在咱們今天關於線程模型的討論中,咱們將首先看一下用當今可用的技術(如Grand Central Dispatch)編寫的一個示例應用程序。而後,咱們將看看一樣的應用程序在用Swift併發性重寫時的表現。併發

假設我想寫我本身的新聞源閱讀器應用。異步

讓咱們來談談個人應用程序的高級組件是什麼。

wwdc2021-10254_hd-0089.png

個人應用程序將有一個主線程,用於驅動用戶界面。

我將有一個數據庫來跟蹤用戶訂閱的新聞源,最後還有一個子系統來處理網絡邏輯,以便重新聞源中獲取最新內容。

讓咱們考慮一下如何用Grand Central Dispatch隊列來構造這個應用程序。

咱們假設用戶要求查看最新的新聞。

wwdc2021-10254_hd-0090.png

在主線程上,咱們將處理用戶事件的手勢。

從這裏,咱們將把請求異步分派到一個處理數據庫操做的串行隊列上。

這樣作的緣由有兩個方面。

首先,經過將工做分配到不一樣的隊列,咱們確保主線程即便在等待潛在的大量工做發生時也能保持對用戶輸入的響應。

其次,對數據庫的訪問是受保護的,由於一個串行隊列保證了相互排斥。

wwdc2021-10254_hd-0091.png

在數據庫隊列中,咱們將遍歷用戶訂閱的新聞源,併爲每一個新聞源安排一個網絡請求到咱們的URLSession,如下載該源的內容。

wwdc2021-10254_hd-0092.png

當網絡請求的結果出現時,URLSession的回調將在咱們的委託隊列中被調用,該隊列是一個併發的隊列。在每一個結果的完成處理程序中,咱們將同步更新數據庫中每一個feeds的最新請求,以便緩存起來供未來使用。最後,咱們會喚醒主線程來刷新用戶界面。

這彷佛是構造這樣一個應用程序的一個徹底合理的方式。咱們已經確保了在處理請求時不會阻塞主線程。經過併發地處理網絡請求,咱們已經利用了程序中固有的並行性。讓咱們仔細看看一個代碼片斷,它顯示了咱們如何處理網絡請求的結果。

wwdc2021-10254_hd-0093.png

首先,咱們建立了一個URLSession,用於執行重新聞源的下載。正如你在這裏看到的,咱們已經將這個URLSession的委託隊列設置爲一個併發隊列。

而後咱們遍歷全部須要更新的新聞源,併爲每一個新聞源在URLSession中安排一個數據任務。在數據任務的完成處理程序中--它將在委託隊列中被調用--咱們對下載的結果進行反序列化,並將其格式化爲文章。

而後,在更新feed的結果以前,咱們針對咱們的數據庫隊列進行同步調度。

因此在這裏你能夠看到,咱們寫了一些線性代碼來作一些至關直接的事情,但這段代碼有一些隱藏的性能陷阱。

爲了進一步瞭解這些性能問題,咱們須要首先深刻了解線程是如何處理GCD隊列的工做的。

wwdc2021-10254_hd-0095.png

在Grand Central Dispatch中,當工做被排入一個隊列時,系統會調出一個線程來處理該工做項目。

因爲一個併發隊列能夠同時處理多個工做項目,系統會啓動多個線程,直到咱們的全部CPU核心都達到飽和。

然而,若是一個線程阻塞了--就像在這裏的第一個CPU核上看到的那樣--而且在併發隊列上還有更多的工做要作,GCD將帶起更多的線程來耗盡剩餘的工做項目。

這樣作的緣由有兩個方面。

首先,經過給你的進程提供另外一個線程,咱們可以確保每一個核心在任什麼時候候都有一個執行工做的線程。這給你的應用程序提供了一個良好的、持續的併發性水平。

其次,被阻塞的線程可能正在等待一個資源,如信號量,而後才能取得進一步進展。被帶入隊列繼續工做的新線程可能可以幫助解開被第一個線程等待的資源。

如今咱們對GCD中的線程提高有了更多的瞭解,讓咱們回頭看看咱們的新聞應用中的CPU執行代碼。

wwdc2021-10254_hd-0097.png

在像Apple Watch這樣的雙核設備上,GCD首先會帶出兩個線程來處理新聞更新結果。 當這些線程在訪問數據庫隊列時受阻,更多的線程被建立以繼續處理網絡隊列。 而後,CPU必須在處理網絡結果的不一樣線程之間進行上下文切換,如不一樣線程之間的白色垂直線所示。

wwdc2021-10254_hd-0099.png

這意味着在咱們的新聞應用中,咱們很容易就會出現很是多的線程。 若是用戶有一百個Feeds須要更新,那麼當網絡請求完成後,每一個URL數據任務都會在併發隊列中有一個完成塊。 當每一個回調在數據庫隊列上阻塞時,GCD會帶出更多的線程,致使應用程序有不少線程。

wwdc2021-10254_hd-0100.png

如今你可能會問,在咱們的應用程序中擁有大量的線程有什麼很差?在咱們的應用程序中擁有大量的線程,意味着系統對線程的過分承諾超過了咱們的CPU核心。

考慮一個有六個CPU核心的iPhone。 若是咱們的新聞應用程序有一百個須要處理的feed更新,這意味着咱們已經用比核心多16倍的線程對iPhone進行了過分配置。這就是咱們所說的線程爆炸現象。 咱們以前的一些WWDC講座已經進一步詳細介紹了與此相關的風險,包括在你的應用程序中出現死鎖的可能性。 線程爆炸還伴隨着內存和調度的開銷,這些開銷可能不會當即顯現出來,因此讓咱們進一步研究一下。

wwdc2021-10254_hd-0101.png

回顧一下咱們的新聞應用,每一個被阻塞的線程在等待再次運行時,都在抓着寶貴的內存和資源。

每一個被阻塞的線程都有一個堆棧和相關的內核數據結構來跟蹤該線程。其中一些線程可能持有其餘正在運行的線程可能須要的鎖。這對於沒有進展的線程來講,是大量的資源和內存的佔用。 因爲線程爆炸,也有更大的調度開銷。 隨着新線程的出現,CPU須要進行全線程上下文切換,以便從舊線程切換到開始執行新線程。 當被阻塞的線程再次變得可運行時,調度員必須在CPU上對線程進行分時,以便它們都能取得進展。

wwdc2021-10254_hd-0103.png

如今,若是這種狀況只發生幾回,線程的分時是沒有問題的--這就是併發的力量。 可是,當出現線程爆炸時,必須在一個核心有限的設備上分時共享數百個線程,會致使過分的上下文切換。這些線程的調度延遲超過了它們會作的有用工做的數量,所以,致使CPU的運行效率也下降。

正如咱們到目前爲止所看到的,在使用GCD隊列編寫應用程序時,很容易錯過一些關於線程衛生的細微差異,從而致使性能不佳和更大的開銷。

Concurrency in Swift

基於這一經驗,Swift在設計語言中的併發性時採起了不一樣的方法。 咱們在構建 Swift 併發時也考慮到了性能和效率,所以你的應用程序能夠享受到可控的、結構化的、安全的併發。 有了Swift,咱們想把應用程序的執行模式從下面這種有不少線程和上下文切換的模式改成這樣。

wwdc2021-10254_hd-0105.png

在這裏你能夠看到,咱們的雙核系統上只有兩個線程在執行,並且沒有線程上下文切換。 咱們全部被阻塞的線程都消失了,取而代之的是一個被稱爲continuation的輕量級對象來跟蹤工做的恢復狀況。 當線程在Swift併發下執行工做時,它們會在**cont.**之間進行切換,而不是執行完整的線程上下文切換。 這意味着咱們如今只須要支付函數調用的成本。

所以,咱們但願Swift併發的運行時行爲是,只建立與CPU核心數量相同的線程,而且線程在被阻塞時可以廉價、高效地在工做項目之間切換。咱們但願你能寫出容易推理的線性型代碼,併爲你提供安全、可控的併發性。

wwdc2021-10254_hd-0106.png

爲了實現咱們所追求的這種行爲,操做系統須要一個運行時契約,即線程不會阻塞,而這隻有在語言可以爲咱們提供這種契約時纔有可能。 所以,Swift的併發模型和圍繞它的語義在設計時就考慮到了這個目標。爲此,我想深刻探討一下Swift語言層面的兩個特色,它們使咱們可以與運行時保持契約關係。

wwdc2021-10254_hd-0107.png

第一個是來自 await 的語義,第二個是來自 Swift 運行時對任務依賴關係的跟蹤。

讓咱們在新聞應用的例子中考慮這些語言特性。

wwdc2021-10254_hd-0108.png

這是咱們以前走過的代碼片斷,它處理了咱們的新聞提要更新的結果。 讓咱們看看這個邏輯在用Swift併發原語編寫時是什麼樣子。

wwdc2021-10254_hd-0109.png

咱們首先會建立一個輔助函數的異步實現。 而後,咱們不在併發調度隊列中處理網絡請求的結果,而是在這裏使用一個任務組來管理咱們的併發性。 在任務組中,咱們將爲每一個須要更新的feed建立子任務。 每一個子任務將使用共享的URLSessionFeedURL上執行下載。 而後,它將反序列化下載的結果,將其格式化爲文章,最後,咱們將調用一個異步函數來更新咱們的數據庫。 在這裏,當調用任何異步函數時,咱們用一個await關鍵字來註釋它。

從 "Meet async/await in Swift "講座中,咱們瞭解到await是一個異步等待。也就是說,在等待異步函數的結果時,它不會阻塞當前線程。相反,該函數可能被暫停,線程將被釋放出來執行其餘任務。

這種狀況是如何發生的?如何放棄一個線程呢?個人同事Varun如今將闡明在Swift運行時的引擎蓋下是如何實現這一目標的。

Await and non-blocking of threads

在討論異步函數是如何實現的以前,咱們先快速回顧一下非異步函數的工做原理。

wwdc2021-10254_hd-0110.png

在一個正在運行的程序中,每一個線程都有一個堆棧,它用來存儲函數調用的狀態。

wwdc2021-10254_hd-0111.png

如今讓咱們專一於一個線程。

wwdc2021-10254_hd-0112.png

當線程執行一個函數調用時,一個新的棧幀被推到它的堆棧上。

wwdc2021-10254_hd-0113.png

這個新建立的堆棧幀能夠被函數用來存儲局部變量、返回地址和任何其餘須要的信息。

wwdc2021-10254_hd-0114.png

一旦函數執行完畢並返回,它的堆棧幀就被彈出。

wwdc2021-10254_hd-0116.png

如今咱們來考慮一下異步函數。

假設一個線程從updateDatabase函數中調用了一個關於Feed類型的add(:)方法。 在這個階段,最近的堆棧幀將是爲add(:)

wwdc2021-10254_hd-0117.png

這個棧幀存儲了不須要跨越暫停點的局部變量。 add(_:)的主體有一個暫停點,用 await 標記。 本地變量,idarticle,在被定義後當即在for循環的主體中使用,中間沒有任何暫停點。因此它們將被存儲在這個棧幀中。

wwdc2021-10254_hd-0118.png

此外,堆上將有兩個異步調用幀,一個用於updateDatabase,一個用於add。異步調用幀存儲的信息確實須要在各暫停點之間可用。

wwdc2021-10254_hd-0119.png

請注意,newArticles參數是在await以前定義的,但須要在await以後纔可用。

這意味着add的異步調用幀 將保持對newArticles的跟蹤。

假設該線程繼續執行。

wwdc2021-10254_hd-0120.png

save函數開始執行時,add的堆棧幀被save的堆棧幀所取代。

不是添加新的堆棧幀,而是替換最上面的堆棧幀,由於任何將來須要的變量都已經存儲在異步調用幀的列表中了。

wwdc2021-10254_hd-0121.png

保存函數也得到了一個異步調用幀供其使用。

當文章被保存到數據庫時,若是線程能作一些有用的工做而不是被阻塞,那就更好了。

wwdc2021-10254_hd-0122.png

假設保存函數的執行被暫停。而線程被從新使用來作一些其餘有用的工做,而不是被阻塞。

由於全部跨越暫停點的信息都存儲在堆上,因此能夠用來在之後的階段繼續執行。

這個異步調用幀的列表是一個Continuation的運行時表示。

wwdc2021-10254_hd-0123.png

假設過了一下子,數據庫請求完成了,假設一些線程被釋放出來。

這多是與以前相同的線程,也多是一個不一樣的線程。

wwdc2021-10254_hd-0124.png

一旦它執行完畢並返回一些ID,那麼save的堆棧幀將再次被add的堆棧幀所取代。

以後,該線程能夠開始執行zip

wwdc2021-10254_hd-0125.png

對兩個數組進行壓縮是一個非異步操做,因此它將建立一個新的堆棧幀。

因爲Swift繼續使用操做系統堆棧,異步和非異步的Swift代碼均可以有效地調用到C和Objective-C。

此外,C和Objective-C代碼能夠繼續有效地調用非異步的Swift代碼。

wwdc2021-10254_hd-0127.png

一旦zip函數完成,它的堆棧幀將被彈出,執行將繼續。

到目前爲止,我已經描述了await是如何設計的,以確保高效的暫停和恢復,同時釋放線程的資源來作其餘工做。

Tracking of dependencies in Swift task model

如前所述,一個函數能夠在一個等待點(也被稱爲潛在的暫停點)被分解成Continuations

wwdc2021-10254_hd-0128.png

在這種狀況下,URLSession數據任務是異步函數,它以後的剩餘工做是Continuations。 只有在異步函數完成後,才能執行Continuations

這是一個由Swift併發運行時跟蹤的依賴關係。

wwdc2021-10254_hd-0129.png

一樣,在任務組中,一個父任務可能會建立幾個子任務,每一個子任務都須要在父任務進行以前完成。 這是一種依賴關係,在你的代碼中經過任務組的範圍來表達,所以明確地被Swift編譯器和運行時所知。 在Swift中,任務只能等待Swift運行時已知的其餘任務--不管是Continuations仍是子任務。

所以,當用Swift的併發原語構建代碼時,運行時會清楚地瞭解任務之間的依賴鏈。

wwdc2021-10254_hd-0131.png

到目前爲止,你已經瞭解到Swift的語言特性是如何容許任務在等待過程當中被暫停的。

相反,執行線程可以對任務的依賴性進行推理,並接上一個不一樣的任務。

這意味着用Swift併發性編寫的代碼能夠維持一個運行時契約,即線程老是可以取得進展。

wwdc2021-10254_hd-0132.png

咱們已經利用這個運行時契約,爲Swift併發性創建了集成的操做系統支持。

這是以一個新的合做線程池的形式,支持Swift併發做爲默認執行器。

新的線程池將只產生與CPU內核相同數量的線程,從而確保不對系統進行過分承諾。

與GCD的併發隊列不一樣,當工做項目受阻時,會產生更多的線程,而Swift的線程老是能夠向前推動。所以,默認運行時能夠明智地控制線程的生成數量。

這讓咱們能夠給你的應用程序提供你須要的併發性,同時確保避免過分併發的已知陷阱。

wwdc2021-10254_hd-0134.png

在之前關於Grand Central Dispatch併發性的WWDC講座中,咱們曾建議你將你的應用程序結構化爲不一樣的子系統,並在每一個子系統中保持一個串行調度隊列,以控制你的應用程序的併發性。 這意味着你很難在一個子系統內得到大於1的併發性,而不會有線程爆炸的風險。

在Swift中,語言爲咱們提供了強大的不變性,運行時利用了這些不變性,從而可以在默認運行時中透明地爲你提供更好的控制併發性。

如今你對Swift併發的線程模型有了更多的瞭解,讓咱們來看看在你的代碼中採用這些使人興奮的新功能時要注意的一些問題。

Adoption of Swift concurrency

wwdc2021-10254_hd-0137.png

你須要記住的第一個考慮因素與將同步代碼轉換爲異步代碼時的性能有關。 早些時候,咱們談到了一些與併發性相關的成本,如Swift運行時的額外內存分配和邏輯。 所以,你須要注意的是,只有當在代碼中引入併發性的成本超過了管理併發性的成本時,纔會用Swift併發性編寫新的代碼。

這裏的代碼片斷實際上可能並無從催生一個子任務的額外併發性中獲益,只是爲了從用戶的默認值中讀取一個值。這是由於子任務所作的有用工做被建立和管理任務的成本削弱了。 所以,咱們建議在採用Swift併發時,用儀器系統跟蹤對你的代碼進行分析,以瞭解它的性能特徵。

wwdc2021-10254_hd-0138.png

第二件須要注意的事情是圍繞await的原子性概念。

Swift並不保證在await以前執行代碼的線程也是將接續的線程。

事實上,await在你的代碼中是一個明確的點,代表原子性被打破了,由於任務可能會被自願取消調度。

所以,你應該注意不要在等待中持有鎖。

wwdc2021-10254_hd-0139.png

一樣地,線程特定的數據也不會在await中被保留下來。

你的代碼中任何指望線程定位的假設都應該被從新審視,以考慮到 await 的暫停行爲。

最後,最後的考慮與運行時契約有關,它是Swift中高效線程模型的基礎。

wwdc2021-10254_hd-0140.png

回顧一下,在Swift中,語言容許咱們堅持一個運行時契約,即線程老是可以向前推動。

正是基於這一契約,咱們創建了一個合做線程池,做爲Swift的默認執行器。

當你採用 Swift 併發時,必須確保在你的代碼中也繼續維護這一契約,以便合做線程池可以以最佳方式運行。

經過使用安全的基元,使你的代碼中的依賴關係明確化和已知化,就有可能在合做線程池中保持這種契約。

wwdc2021-10254_hd-0141.png

有了Swift併發原語,好比awaitactors任務組,這些依賴關係在編譯時就已經被知道了。所以,Swift編譯器會強制執行這一點,並幫助你保留運行時契約。

os_unfair_locksNSLocks這樣的原語也是安全的,但在使用它們時須要謹慎。在同步代碼中使用鎖是安全的,當用於圍繞一個緊密的、衆所周知的關鍵部分進行數據同步時。這是由於持有鎖的線程老是可以在釋放鎖的過程當中取得進展。所以,雖然該基元可能會在競爭中阻斷線程一小段時間,但它並不違反向前推動的運行時契約。值得注意的是,與Swift併發原語不一樣,沒有編譯器支持來幫助正確使用鎖,因此正確使用這一原語是你的責任。

wwdc2021-10254_hd-0142.png

另外一方面,像semaphores條件變量這樣的基元,在Swift併發中使用是不安全的。這是由於它們向Swift運行時隱藏了依賴性信息,但在你的代碼中執行時卻引入了依賴性。因爲運行時不知道這種依賴關係,因此它沒法作出正確的調度決策並解決這些問題。特別是,不要使用建立非結構化任務的原語,而後經過使用信號量或不安全的原語,追溯性地引入跨任務邊界的依賴關係。這樣的代碼模式意味着一個線程能夠無限期地阻塞信號量,直到另外一個線程可以解除阻塞。這違反了線程向前推動的運行時契約。

wwdc2021-10254_hd-0143.png

爲了幫助你識別代碼庫中這種不安全基元的使用,咱們建議用如下環境變量測試你的應用程序。這將在修改過的調試運行時下運行你的應用程序,該運行時強制執行向前推動的不變量。 這個環境變量能夠在Xcode中設置在你的項目方案的Run Arguments窗格中,如圖所示。

wwdc2021-10254_hd-0144.png

wwdc2021-10254_hd-0145.png

wwdc2021-10254_hd-0146.png

當用這個環境變量運行你的應用程序時,若是你看到一個來自合做線程池的線程彷佛被掛起,這代表使用了一個不安全的阻塞原語。

Synchronization

mutual exclusion

如今,在瞭解了線程模型是如何爲Swift併發性設計的以後,讓咱們再來了解一下在這個新世界中可用於同步狀態的基元。

在關於Actor的Swift併發性講座中,你已經看到了Actor是如何被用來保護易變的狀態不被併發訪問的。

換句話說,Actor提供了一個強大的新同步原語,你可使用。

回顧一下,Actor保證了相互排斥:一個Actor在同一時間最多隻能執行一個方法調用。相互排斥意味着Actor的狀態不會被同時訪問,從而防止數據競爭。

讓咱們看看Actor與其餘形式的互斥相好比何。

wwdc2021-10254_hd-0147.png

考慮一下前面的例子,經過同步到一個串行隊列來更新數據庫中的一些文章。 若是隊列尚未運行,咱們就說不存在競爭。 在這種狀況下,調用線程被重用來執行隊列上的新工做項目,而沒有任何上下文切換。 相反,若是序列隊列已經在運行,則稱該隊列處於爭用狀態。 在這種狀況下,調用線程會被阻塞。 這種阻塞行爲就是以前演講中早先描述的引起線程爆炸的緣由。 鎖也有這種行爲。

wwdc2021-10254_hd-0148.png

因爲與阻塞有關的問題,咱們一般建議你最好使用Dispatch asyncDispatch async的主要好處是它是無阻塞的。 所以,即便在爭用的狀況下,它也不會致使線程爆炸。 在串行隊列中使用Dispatch async的缺點是,當沒有競爭的時候,Dispatch須要請求一個新的線程來作異步工做,而調用線程則繼續作其餘事情。 所以,頻繁使用Dispatch async會致使過多的線程喚醒和上下文切換。

這就給咱們帶來了Actor

SwiftActor利用合做線程池的優點進行有效的調度,從而結合了這兩個世界的優勢。 當你在一個沒有運行的Actor上調用一個方法時,調用線程能夠被重用來執行方法調用。 在被調用的Actor已經在運行的狀況下,調用線程能夠暫停它正在執行的函數,並接上其餘工做。

wwdc2021-10254_hd-0149.png

讓咱們看看這兩個屬性在新聞應用的例子中是如何工做的。 咱們來關注一下數據庫和網絡子系統。

wwdc2021-10254_hd-0150.png

當更新應用程序以使用Swift併發時,數據庫的串行隊列將被一個數據庫Actor所取代。 網絡的併發隊列能夠被每一個新聞源的一個Actor所取代。爲了簡單起見,我在這裏只展現了三個Actor--體育Actor、天氣Actor和健康Actor--但在實踐中,會有更多的Actor。 這些Actor將在合做線程池中運行。 feedActor與數據庫互動,以保存文章和執行其餘動做。 這種互動涉及到從一個Actor到另外一個Actor的執行切換。

咱們稱這個過程爲Actor跳轉。 讓咱們來討論一下Actor跳轉是如何進行的。

假設體育頻道的Actor在合做線程池中的一個線程上運行,它決定將一些文章保存到數據庫中。

wwdc2021-10254_hd-0151.png

如今,讓咱們考慮數據庫沒有被使用。 這是不存在競爭的狀況。

wwdc2021-10254_hd-0152.png

線程能夠直接從體育頻道的Actor跳到數據庫的Actor

這裏有兩件事須要注意。 首先,線程在跳轉Actor時沒有阻塞。 第二,跳轉不須要不一樣的線程;運行時能夠直接暫停體育節目Actor的工做項目,爲數據庫Actor建立一個新的工做項目。

wwdc2021-10254_hd-0153.png

假設數據庫Actor運行了一段時間,但它尚未完成第一個工做項。在這個時候,假設天氣預報Actor試圖在數據庫中保存一些文章。

這就爲數據庫Actor創造了一個新的工做項目。Actor經過保證相互排斥來確保安全;在給定的時間內,最多隻有一個工做項是活動的。 因爲已經有一個活動的工做項目D1,新的工做項目D2將被保留。

wwdc2021-10254_hd-0154.png

Actor也是無阻塞的。在這種狀況下,天氣預報Actor將被暫停,它所執行的線程如今被釋放出來作其餘工做。

wwdc2021-10254_hd-0155.png

過了一下子,最初的數據庫請求完成了,因此數據庫Actor的活動工做項被移除。

在這一點上,運行時能夠選擇開始執行數據庫Actor的未決工做項目。 或者它能夠選擇恢復一個進位Actor。 或者它能夠在被釋放的線程上作一些其餘工做。

Reentrancy and prioritization

當有不少異步工做,特別是有不少爭論時,系統須要根據什麼工做更重要來進行權衡。 理想狀況下,高優先級的工做,如涉及用戶互動的工做,將優先於後臺工做,如保存備份。 因爲重入的概念,Actor被設計成容許系統很好地安排工做的優先次序。 可是爲了理解爲何重入性在這裏很重要,讓咱們先看看GCD是如何處理優先級的。

wwdc2021-10254_hd-0156.png

考慮一下帶有串行數據庫隊列的原始新聞應用。 假設數據庫收到了一些高優先級的工做,好比獲取最新數據以更新用戶界面。 它也會收到低優先級的工做,例如將數據庫備份到iCloud。 這須要在某個時間點完成,但不必定是當即完成。 隨着代碼的運行,新的工做項目被建立並以某種交錯的順序添加到數據庫隊列中。 Dispatch Queue以嚴格的先入先出順序執行收到的項目。 不幸的是,這意味着在項目A執行完後,在進入下一個高優先級項目以前,須要執行五個低優先級的項目。 這就是所謂的優先級倒置。

wwdc2021-10254_hd-0157.png

串行隊列經過提升隊列中全部在高優先級工做以前的工做的優先級來解決優先級倒置的問題。 在實踐中,這意味着隊列中的工做將更快完成。 然而,這並無解決主要問題,即在項目B開始執行以前,項目1到5仍然須要完成。 解決這個問題須要改變語義模型,使其脫離嚴格的先進先出。

這就把咱們帶到了Actor的重入。 讓咱們經過一個例子來探索重入是如何與排序相聯繫的。

wwdc2021-10254_hd-0158.png

考慮一下在一個線程上執行的數據庫Actor。 假設它被暫停,等待一些工做,而體育節目的Actor開始在該線程上執行。 假設過了一下子,體育頻道的Actor調用數據庫Actor來保存一些文章。 因爲數據庫Actor是未被徵用的,線程能夠跳到數據庫Actor上,儘管它有一個待處理的工做項目。 爲了執行保存操做,將爲數據庫Actor建立一個新的工做項。

wwdc2021-10254_hd-0159.png

這就是actor reentrancy的含義;當一個Actor上的一個或多箇舊的工做項被暫停時,該Actor上的新工做項能夠取得進展。 Actor仍然保持相互排斥:在一個給定的時間內最多隻能有一個工做項在執行。

wwdc2021-10254_hd-0160.png

一段時間後,項目D2將完成執行。 注意,D2在D1以前完成了執行,儘管它是在D1以後建立的。 所以,對Actor重入的支持意味着Actor能夠按照不是嚴格意義上的先入先出的順序執行項目。

wwdc2021-10254_hd-0161.png

讓咱們再來看看以前的例子,但要用一個數據庫Actor而不是一個序列隊列。 首先,工做項目A將被執行,由於它有很高的優先級。 一旦執行完畢,就會出現和以前同樣的優先級倒置。

wwdc2021-10254_hd-0162.png

因爲Actor是爲重入設計的,運行時能夠選擇將優先級較高的項目移到隊列的前面,排在優先級較低的項目前面。 這樣一來,較高優先級的工做就能夠先執行,較低優先級的工做則在後面。 這直接解決了優先級倒置的問題,容許更有效的調度和資源利用。 我已經談到了一些關於使用合做線程池的Actor是如何被設計來維持相互排斥和支持有效的工做優先級的。

Main actor

還有一種Actor,即MainActor,其特色有些不一樣,由於它抽象了系統中的一個現有概念:主線程。

考慮一下使用Actor的新聞應用程序的例子。

wwdc2021-10254_hd-0163.png

當更新用戶界面時,你須要對MainActor進行調用,也須要從MainActor進行調用。 因爲主線程與合做線程池中的線程是不相干的,這須要進行上下文切換。 讓咱們經過一個代碼例子來看看這其中的性能影響。 考慮下面的代碼,咱們在MainActor上有一個函數updateArticles,它從數據庫中加載文章,併爲每篇文章更新UI。

wwdc2021-10254_hd-0164.png

循環的每一次迭代都須要至少兩次上下文切換:一次是從MainActor跳到數據庫Actor,另外一次是跳回來。 讓咱們看看這樣一個循環的CPU使用率是怎樣的。

wwdc2021-10254_hd-0165.png

因爲每一個循環迭代都須要兩次上下文切換,因此會出現一個重複的模式,即兩個線程在短期內相繼運行。 若是循環迭代的數量很少,並且每次迭代都在作大量的工做,這多是正確的。

wwdc2021-10254_hd-0166.png

然而,若是執行過程當中頻繁地在MainActor上跳來跳去,切換線程的開銷就會開始增長。 若是你的應用程序在上下文切換中花費了大量的時間,你應該重組你的代碼,使MainActor的工做被分批進行。

wwdc2021-10254_hd-0167.png

你能夠經過把循環推到loadArticlesupdateUI方法調用中,確保它們處理數組而不是一次處理一個值來分批工做。 分批工做能夠減小上下文切換的次數。 雖然在合做線程池上的Actor之間跳轉很迅速,但在編寫應用程序時,你仍然須要注意與MainActor之間的跳轉。

總結

回顧過去,在此次演講中,你已經瞭解到咱們是如何努力使系統達到最高效的,從合做線程池的設計--非阻塞等待機制--到如何實現Actor。 在每一個步驟中,咱們都在使用運行時契約的某些方面來提升你的應用程序的性能。 咱們很高興看到你如何使用這些使人難以置信的新語言特性來編寫清晰、高效和使人愉悅的Swift代碼。 謝謝你的觀看,祝你有一個美好的WWDC。

相關文章
相關標籤/搜索