Swift推出了一個新的類型actor來保護可變狀態,使得在併發編程中避免數據競爭,使用關鍵字可使編譯器前置檢查潛在的併發風險,寫出更健壯的代碼。編程
從幾個例子看數據競爭以及如何避免數組
當兩個獨立的線程同時訪問相同的數據,而且其中至少有一個訪問是寫入時,就會發生數據競賽。緩存
數據競賽的構造很簡單,但倒是出了名的難以調試。這裏有一個簡單的計數器類,它的一個操做是增長計數器並返回其新值。假設咱們繼續前進並嘗試從兩個併發的任務中增量。根據執行的時間,咱們可能獲得1而後2,或者2而後1。這是預料之中的,在這兩種狀況下,計數器都會被留在一個一致的狀態。但因爲咱們引入了數據競賽,若是兩個任務都讀0寫1,咱們也可能獲得1和1。安全
或者,若是返回語句發生在兩個增量操做以後,甚至是2和2。數據競賽是出了名的難以免和調試的。服務器
值語義類型的 "let "屬性是真正不可變的,因此從不一樣的併發任務中訪問它們是安全的。試圖使用值類型解決數據競爭,把咱們的計數器變成一個結構,使它成爲一個值類型。markdown
咱們還必須將增量函數標記爲mutating的,使它能夠修改值屬性。還須要把counter變量改爲var,使其成爲可變的,這又回到了數據競爭的困境中。閉包
除非使用鎖或者串行隊列來保證原子性,下面咱們看actor是如何解決的。併發
Actor提供一種共享可改變狀態的同步機制。框架
一個Actor有它本身的狀態,這個狀態與程序的其餘部分隔離。訪問該狀態的惟一途徑是經過Actor。異步
只要你經過Actor,Actor的同步機制就會確保沒有其餘代碼在同時訪問Actor的狀態。這給了咱們與手動使用鎖或串行調度隊列相同的互斥屬性,但對於Actor來講,這是Swift提供的一個基本保證。
Actor是Swift中的一種新類型。它們提供了與Swift中全部命名類型相同的能力。
它們能夠有屬性、方法、初始化器、下標,等等。它們能夠符合協議,也能夠用擴展來加強。
像類同樣,它們是引用類型;由於Actor的目的是表達共享的可變狀態。
事實上,Actor類型的主要特徵是它們將其實例數據與程序的其餘部分隔離,並確保對這些數據的同步訪問。
咱們又有兩個併發的任務試圖對同一個計數器進行增量。Actor的內部同步機制確保一個增量調用在另外一個開始以前執行完畢。因此咱們能夠獲得1和2或者2和1,由於這兩個都是有效的併發執行,可是咱們不能兩次獲得相同的計數或者跳過任何數值,由於Actor的內部同步已經消除了Actor狀態的數據競賽的可能性。
讓咱們考慮一下,當兩個併發任務都試圖同時增長計數器時,實際會發生什麼。一個會先到,而另外一個則必須等待輪到。但咱們如何確保第二個任務可以耐心地等待輪到它的Actor呢?Swift有一個這樣的機制。每當你從外部與Actor交互時,你都是異步進行的。 若是Actor很忙,那麼你的代碼就會暫停,這樣你運行的CPU就能夠作其餘有用的工做。 當Actor再次變得空閒時,它將喚醒你的代碼--恢復執行--以便調用能夠在Actor上運行。
這個例子中的 await 關鍵字代表,對Actor的異步調用可能涉及到這樣一個暫停。讓咱們再進一步擴展咱們的反例,增長一個沒必要要的緩慢的重置操做。這個操做將值設置爲0,而後調用適當次數的增量,使計數器達到新值。 這個resetSlowly方法被定義在計數器Actor類型的擴展中,因此它是在Actor內部。 這意味着它能夠直接訪問Actor的狀態,它就是這樣作的,將計數器的值重置爲0。
它還能夠同步地調用Actor上的其餘方法,好比調用increment。 這不須要等待,由於咱們已經知道咱們是在Actor上運行。
這是Actor的一個重要屬性。
Actor上的同步代碼老是在不被打斷的狀況下運行到完成。
所以,咱們能夠按順序推理同步代碼,而不須要考慮併發對咱們Actor狀態的影響。
咱們已經強調了咱們的同步代碼是不間斷運行的,可是Actor之間或與系統中的其餘異步代碼之間常常進行交互。讓咱們花幾分鐘的時間來談談異步代碼和Actor。
它負責從其餘服務中下載圖像。它還將下載的圖像存儲在一個緩存中,以免屢次下載同一圖像。
邏輯流程很簡單:檢查緩存,下載圖片,而後在返回以前將圖片記錄在緩存中。由於咱們是在一個Actor中,這段代碼沒有低級的數據競賽;任何數量的圖像均可以被同時下載。Actor的同步機制保證每次只有一個任務能夠執行訪問緩存實例屬性的代碼,因此緩存不可能被破壞。也就是說,這裏的 await 關鍵字在傳達一些很是重要的信息。每當await發生時,就意味着這個函數在此時能夠被暫停。
它放棄了本身的CPU,因此程序中的其餘代碼能夠執行,這影響了整個程序的狀態。在你的函數恢復的時候,整個程序的狀態將發生變化。
重要的是要確保你在等待以前沒有對該狀態作出假設,而這些假設在等待以後可能不成立。
想象一下,咱們有兩個不一樣的併發任務,試圖在同一時間獲取同一圖像。第一個任務看到沒有緩存條目,開始從服務器上下載圖片,而後由於下載須要一段時間而被暫停。
當第一個任務正在下載圖像時,一個新的圖像可能被部署到服務器上,在同一個URL下。
如今,第二個併發任務試圖從該URL下獲取圖像。
它也沒有看到緩存條目,由於第一個下載尚未完成,而後開始第二次下載圖像。
當它的下載完成時,它也被暫停。
過了一下子,其中一個下載--讓咱們假設是第一個--將完成,它的任務將在Actor上恢復執行。
它填充了緩存,並返回所獲得的貓的圖像。
如今第二個任務完成了它的下載,因此它被喚醒。
它用它獲得的那隻悲傷的貓的圖像覆蓋了緩存中的同一條目。
所以,儘管緩存中已經有了一張圖片,但咱們如今在同一個URL上獲得了一張不一樣的圖片。
這就有點讓人吃驚了。
咱們指望,一旦咱們緩存了一張圖片,咱們老是能在同一個URL上獲得相同的圖片,這樣咱們的用戶界面就會保持一致,至少在咱們去手動清除緩存以前是這樣。可是在這裏,緩存的圖片意外地發生了變化。
咱們沒有任何低級別的數據競賽,可是由於咱們在等待中攜帶了關於狀態的假設,咱們最終出現了一個潛在的錯誤。
這裏的修復方法是在等待後檢查咱們的假設。
若是當咱們恢復時,緩存中已經有一個條目,咱們就保留原來的版本,丟棄新的。一個更好的解決方案是徹底避免多餘的下載。
Actor重入能夠防止死鎖,並保證向前推動,但它要求你在每一個等待中檢查你的假設。
爲了更好地設計重入性,在同步代碼中執行Actor狀態的修改。
最好是在一個同步函數中進行,這樣全部的狀態變化都被很好地封裝起來。
狀態的改變可能涉及到將咱們的Actor暫時置於一個不一致的狀態。
請確保在等待以前恢復一致性。
記住,await是一個潛在的暫停點。
若是你的代碼被暫停,程序和世界會在你的代碼被恢復以前繼續前進。
你對全局狀態、時鐘、計時器或你的Actor所作的任何假設,都須要在await以後進行檢查。
在本節中,咱們將討論Actor隔離如何與其餘語言特性互動,包括協議符合性、閉包和類。像其餘類型同樣,只要Actor可以知足協議的要求,它們就能夠符合協議。
例如,讓咱們讓這個LibraryAccount Actor符合Equatable協議。靜態的相等方法根據兩個圖書館帳戶的ID號碼進行比較。
由於該方法是靜態的,沒有自我實例,因此它不是孤立於Actor的。相反,咱們有兩個Actor類型的參數,而這個靜態方法是在這兩個參數以外的。這不要緊,由於這個實現只是在訪問Actor的不可變的狀態。
讓咱們進一步擴展咱們的例子,使咱們的LibraryAccount符合Hashable協議。這樣作須要實現**hash(in)**操做,咱們能夠像這樣作。
然而,Swift編譯器會抱怨說這種一致性是不容許的。
發生了什麼?嗯,這樣符合Hashable意味着這個函數能夠從Actor外部調用,可是hash(in)不是異步的,因此沒有辦法保持Actor的隔離。
爲了解決這個問題,咱們能夠把這個方法變成非隔離的。
非隔離的意思是,這個方法被視爲在Actor以外,即便它在語法上是在Actor上描述的。
這意味着它能夠知足Hashable協議的同步要求。
由於非隔離的方法被視爲在Actor以外,因此它們不能引用Actor上的可變狀態。
這個方法是好的,由於它引用的是不可變的ID號。
若是咱們試圖基於其餘東西進行哈希操做,好比說借閱的書籍數組,咱們會獲得一個錯誤,由於從外部訪問易變的狀態會容許數據競賽。
協議符合性的問題就到此爲止。
讓咱們來談一談閉包。閉包是在一個函數中定義的小函數,而後能夠被傳遞給另外一個函數,在一段時間後被調用。
像函數同樣,閉包多是Actor隔離的,也多是非隔離的。
在這個例子中,咱們要從咱們借來的每本書中讀出一些,並返回咱們讀過的總頁數。
對reduce的調用涉及一個執行閱讀的閉包。
注意,在對 readSome 的調用中沒有 await。
這是由於這個閉包是在與Actor隔離的函數 "read "中造成的,它自己就是與**(actor-isolated)Actor**隔離的。
咱們知道這是安全的,由於reduce操做將同步執行,而且不能將閉包轉移到其餘線程中,以避免形成併發訪問。
如今,讓咱們作一點不一樣的事情。
我如今沒有時間讀,因此咱們之後再讀。
在這裏,咱們建立一個分離式任務。
一個分離的任務在執行閉包的同時,還執行Actor正在進行的其餘工做。
所以,這個閉包不能在Actor上,不然咱們會引入數據競賽。
因此這個閉包並非孤立於Actor的。
當它想調用讀取方法時,它必須以異步方式進行,正如 await 所示。
咱們已經談了一些關於Actor隔離的代碼,也就是這些代碼是在Actor內部仍是外部運行。
如今,讓咱們來談談Actor隔離和數據的問題。
在咱們的圖書館帳戶的例子中,咱們刻意避免了說書的類型究竟是什麼。
我一直假設它是一個值類型,像一個結構。
這是一個很好的選擇,由於它意味着圖書館帳戶Actor實例的全部狀態都是獨立的。
若是咱們繼續調用這個方法來隨機選擇一本書來閱讀,咱們會獲得一個咱們能夠閱讀的書的副本。
咱們對書的副本所作的改變不會影響到Actor,反之亦然。
然而,若是把這本書變成一個類,事情就有點不一樣了。
咱們的圖書館帳戶Actor如今引用書的類的實例。
這自己並非一個問題。
然而,當咱們調用這個方法來選擇一本隨機書時,會發生什麼呢?如今咱們有一個對Actor的可變狀態的引用,這個引用已經在Actor以外被共享。咱們已經創造了數據競賽的可能性。
如今,若是咱們去更新書的標題,修改發生在Actor內部可訪問的狀態中。
由於訪問方法不在Actor上,因此這個修改最終可能成爲數據競賽。
值類型和Actor在併發使用時都是安全的,但類仍然會帶來問題。
咱們爲能夠安全地併發使用的類型起了一個名字。Sendable可發送類型。
一個Sendable可發送的類型是一個其值能夠在不一樣的Actor之間共享的類型。
若是你把一個值從一個地方複製到另外一個地方,而且兩個地方均可以安全地修改他們本身的值的副本而不互相干擾,那麼這個類型就是可發送的。
值類型是可發送的,由於每一個副本都是獨立的,正如在前面談到的。
Actor類型是可發送的,由於它們同步訪問它們的可改變的狀態。
類能夠是可發送的,但只有在它們被仔細實現的狀況下。
例如,若是一個類和它的全部子類只持有不可變的數據,那麼它就能夠被稱爲可發送。
或者,若是該類在內部執行同步,例如使用鎖,以確保安全的併發訪問,那麼它就能夠是可發送的。
可是大多數類都不是這樣的,因此不能被稱爲可發送類。
函數不必定是可發送的,因此有一種新的函數類型,能夠安全地跨Actor傳遞。
你的Actor--事實上,你全部的併發代碼--應該主要以可發送類型進行通訊。可發送類型能夠保護代碼免受數據競賽的影響。
這是一個Swift最終會開始靜態檢查的屬性。
到那時,跨越Actor邊界傳遞非可發送類型將成爲一個錯誤。
人們如何知道一個類型是可發送的呢?好吧,Sendable是一個協議,你聲明你的類型符合Sendable,就像你對其餘協議所作的同樣。
而後 Swift 會檢查以確保你的類型做爲可發送類型是合理的。
若是一個圖書結構的全部存儲屬性都是可發送類型,那麼它就能夠是可發送的。
比方說,做者其實是一個類,這意味着它--以及做者數組--不是可發送的。
Swift會產生一個編譯器錯誤,代表Book不能是可發送的。
對於泛型類型,它們是不是可發送的,可能取決於它們的泛型參數。
咱們能夠在適當的時候使用條件一致性來傳播Sendable。
例如,只有當一個對類型的兩個泛型參數都是可發送的,它纔是可發送的。
一樣的方法被用來得出結論,一個可發送類型的數組自己就是可發送的。
咱們鼓勵你將Sendable的符合性引入到其值能夠安全地併發共享的類型中。
在你的Actor中使用這些類型。
而後,當 Swift 開始執行跨Actor的 Sendable 時,你的代碼就準備好了。函數自己能夠是可發送的,這意味着跨Actor傳遞函數值是安全的。
這對閉包尤爲重要,由於它限制了閉包能夠作的事情,以幫助防止數據競賽。
例如,一個可發送的閉包不能捕獲一個可變的局部變量,由於這將容許局部變量的數據競賽。
閉包捕獲的任何東西都須要是可發送的,以確保閉包不能被用來跨Actor邊界移動非可發送類型。
最後,一個同步的Sendable閉包不能被(actor-isolated)Actor隔離,由於這將容許代碼從外部運行到Actor上。
在此次演講中,咱們實際上一直在依賴Sendable閉包的想法。
建立分離任務的操做須要一個Sendable函數,這裏寫的是函數類型中的**@Sendable**。
還記得咱們在講座開始時的反例嗎?咱們正試圖創建一個值類型的計數器。
而後,咱們試圖同時從兩個不一樣的閉包中去修改它。
這將是一個關於可變局部變量的數據競賽。
然而,因爲分離任務的閉包是Sendable,Swift會在這裏產生一個錯誤。
可發送的函數類型被用來指示哪裏能夠併發執行,從而防止數據競賽。
下面是咱們以前看到的另外一個例子。
由於分離任務的閉包是可發送的,咱們知道它不該該被隔離到Actor中。
所以,與它的交互必須是異步的。
可發送類型和閉包經過檢查易變狀態是否在Actor之間共享,以及是否不能被併發修改來幫助維護Actor的隔離。
咱們一直在討論Actor類型,以及它們如何與協議、閉包和可發送類型交互。
還有一個Actor要討論 -- 一個特殊的Actor,咱們稱之爲MainActor。
當你構建一個應用程序時,你須要考慮到主線程。
它是核心用戶界面渲染髮生的地方,也是處理用戶交互事件的地方。
與用戶界面有關的操做一般須要在主線程中執行。
然而,你不但願在主線程上作全部的工做。
若是你在主線程上作了太多的工做,好比說,由於你有一些緩慢的輸入/輸出操做或與服務器的阻塞交互,你的用戶界面會凍結。
所以,你須要注意在主線程與用戶界面交互時,在主線程上作工做,但在計算成本高或等待時間長的操做中,要迅速離開主線程。
因此,咱們在能夠的時候從主線程上作工做,而後調用DispatchQueue.main.async。
只要你有一個必須在主線程上執行的特定操做,就能夠在你的代碼中使用async。
從機制的細節中回過頭來看,這段代碼的結構看起來隱約有些熟悉。
事實上,與主線程的交互就像與一個Actor的交互。
若是你知道你已經在主線程上運行,你能夠安全地訪問和更新你的用戶界面狀態。
若是你不在主線程上運行,你須要與它進行異步交互。
這正是演員的工做方式。
有一個特殊的Actor來描述主線程,咱們稱之爲MainActor。MainActor是一個表明主線程的Actor。
它在兩個重要方面與普通的Actor不一樣。
首先,MainActor經過主調度隊列來執行全部的同步。
這意味着,從運行時的角度來看,MainActor能夠與使用DispatchQueue.main互換
第二,須要在主線程上的代碼和數據散佈在各個地方。
它在SwiftUI、AppKit、UIKit和其餘系統框架中。
它分散在你本身的視圖、視圖控制器和你的數據模型中面向用戶界面的部分。
利用Swift併發性,你能夠用MainActor屬性來標記一個聲明,表示它必須在main actor上執行。
咱們在這裏對checkedOut操做作了這樣的標記,因此它老是在MainActor上運行。
若是你從MainActor以外調用它,你須要等待,這樣調用就會在主線程上異步執行。
經過將必須在主線程上運行的代碼標記爲在MainActor上運行,就不須要再猜想什麼時候使用DispatchQueue.main了。
Swift確保這些代碼老是在主線程上執行。
類型也能夠放在MainActor上,這使得它們全部的成員和子類都在MainActor上。
這對於你的代碼庫中必須與UI交互的部分頗有用,在這些部分中,大多數東西都須要在主線程上運行。
個別方法能夠經過nonisolated關鍵字選擇退出,其規則與你所熟悉的普通Actor相同。
經過對面向用戶界面的類型和操做使用MainActor,並引入本身的Actor來管理其餘程序狀態,你能夠構建你的應用程序,以確保安全、正確地使用併發性。
在此次會議中,咱們談到了Actor如何使用**(Actor isolation)Actor 隔離和要求來自Actor**外部的異步訪問來序列化執行,從而保護他們的可變狀態免受併發訪問。
使用actor在你的Swift代碼中創建安全、併發的抽象。
在實現你的Actor和任何異步代碼時,要始終爲重入性而設計;你的代碼中的等待意味着世界能夠繼續前進,並使你的假設失效。
值類型和Actor一塊兒工做,以消除數據競賽。
要注意那些不處理本身的同步的類,以及其餘從新引入共享可變狀態的非Sendable類型。
最後,在你與UI交互的代碼上使用main actor,以確保必須在主線程上運行的代碼老是在主線程上運行。