[譯] Swift 中關於併發的一切:第一部分 — 當前

Swift 中關於併發的一切:第一部分 — 當前

在 Swift 語言的當前版本中,並無像其餘現代語言如 Go 或 Rust 同樣,包含任何原生的併發功能。html

若是你計劃異步執行任務,而且須要處理由此產生的競爭條件時,你惟一的選擇就是使用外部庫,好比 libDispatch,或者 Foundation 和 OS 提供的同步原語。前端

在本系列教程的第一部分,咱們會介紹 Swift 3 提供的功能,涵蓋一切,從基礎鎖、線程和計時器,到語言守護和最近改善的 GCD 和操做隊列。react

咱們也會介紹一些基礎的併發概念和一些常見的併發模式。android

klingon 示例代碼中的關鍵部分
klingon 示例代碼中的關鍵部分

即便 pthread 庫的函數和原語能夠在任一個運行 Swift 的平臺上使用,咱們也不會在這裏討論,由於對於每一個平臺,都有更高級的方案。ios

NSTimer 類也不會在這裏介紹,你能夠看一看這裏,來了解如何在 Swift 3 中使用它。git

就像已屢次公佈的,Swift 4.0 以後的主要版本之一(不必定是 Swift 5)會擴展語言的功能,更好地定義內存模型,幷包含了新的原生併發功能,能夠不須要藉助外部庫來處理併發,實現並行化,定義了一種 Swift 方式來實現併發。github

這是本系列下一篇文章討論的內容,咱們會討論一些其餘語言實現的替代方法和範式實現,和在 Swift 中他們是如何實現的。而且咱們會分析一些用當前版本 Swift 完成的開源實現,這些實現中咱們可使用 Actor 範式,Go 的 CSP 通道,軟件事務內存等特性。編程

第二篇文章將會徹底是推測性的,它主要的目的是爲你介紹這些主題,以便你之後能夠參與到更熱烈討論當中,而這些討論將會定義將來 Swift 版本的併發是怎麼處理的。swift

本文或其餘文章的 playground 能夠在 GitHubZipped 找到。

目錄

多線程與併發入門

如今,不管你構建的是哪種應用,你早晚會考慮應用在多線程環境運行的狀況。

具備多個處理器或者多核處理器的計算平臺已經存在了幾十年,而像 threadprocess 這樣的概念甚至更久。

操做系統已經經過各類方式開放了這些能力給用戶的程序,每一個現代的框架或者應用都會實現一些涉及多線程的廣爲人知的設計模式,來提升程序的性能與靈活性。

在咱們開始鑽研如何處理 Swift 併發的細節以前,讓我先簡要地解釋幾個你須要知道的概念,而後再開始考慮你是使用
Dispatch Queues 仍是 Operation Queues

首先,你可能會問,雖然 Apple 的平臺和框架使用了線程,可是我爲何要在本身的應用中引入它們呢?

有一些常見的狀況,讓多線程的使用合情合理:

  • 任務組分離: 線程能從執行流程的角度,模塊化你的程序。不一樣的線程用可預測方式,執行一組相同的任務,把他們與你程序的其餘執行流程部分隔離,這樣你會更容易理解程序當前的狀態。

  • 獨立數據的計算並行化: 可使用由硬件線程支持的多個軟件線程(能夠參考下一條),來並行化在原始輸入數據結構的子集上運行的相同任務的多個副本。

  • 等待條件達成或 I/O 的一種簡潔的實現方式: 在執行 I/O 阻塞或其餘類型的阻塞操做時,可使用後臺線程來乾淨地等待這些操做完成。使用線程能夠改進你程序的總體設計,而且使處理阻塞問題變成細枝末節的事情。

可是,當多個線程執行你應用的代碼時,一些從單線程的角度看起來無心義的假設就變得很是重要了。

在每一個線程都獨立地執行且沒有數據共享的完美狀況下,併發編程實際上並不比編寫單線程執行的代碼複雜多少。可是,就像常常發生的那樣,你打算用多個線程操做同一數據,那就須要一種方式來規劃對這些數據結構的訪問,以確保該數據上的每一個操做都按預期完成,而不會與其餘線程有任何的交互操做。

併發編程須要來自語言和操做系統的額外保證,須要明確地說明在多個線程同時訪問變量(或更通常的稱之爲「資源」)並嘗試修改他們的值時,他們的狀態是如何變化的。

語言須要定義一個內存模型,一組規則明確地列出在併發線程的運行下一些基本語句的行爲,而且定義如何共享內存以及哪一種內存訪問是有效的。

多虧了這個(內存模型),用戶有了一個線程運行行爲可預知的語言,而且咱們知道編譯器將僅對遵循內存模型中定義的內容進行優化。

定義內存模型是語言進化的一個精妙的步驟,由於太嚴格的模型可能會限制編譯器的自身發展。對於內存模型的過去策略,新的巧妙的優化會變得無效。

定義內存模型的例子:

  • 語言中哪些語句能夠被認爲是原子性的,哪些不是,哪些操做只能做爲一個總體執行,其它線程看不到中間結果。好比必須知道變量是否被原子地初始化。

  • 如何處理變量在線程之間的共享,他們是否被默認緩存,以及他們是否會對被特定語言修飾符修飾的緩存行爲產生影響。

  • 例如,用於標記和規劃訪問關鍵部分(那些操做共享資源的代碼塊)的併發操做符一次只容許一個線程訪問一個特定的代碼路徑。

如今讓咱們回頭聊聊在你程序中併發的使用。

爲了正確處理併發問題,你要標識程序中的關鍵部分,而後用併發原語或併發化的數據結構來規劃數據在不一樣線程之間的共享。

對代碼或數據結構這些部分的強制訪問規則打開了另外一組問題,這些源於事實的問題就是,雖然指望的結果是每一個線程都可以被執行,並有機會修改共享數據,可是在某些狀況下,其中一些可能根本沒法執行,或者數據可能以意想不到的和不可預測的方式改變。

你將面臨一系列額外的挑戰,而且必須處理一些常見的問題:

  • 競爭條件: 同一數據上多個線程的操做,例如併發地讀寫,一系列操做的執行結果可能會變得沒法預測,而且依賴於線程的執行順序。

  • 資源爭奪: 多個線程執行不一樣的任務,在嘗試獲取相同資源的時候,會增長安全獲取所需資源的時間。獲取這些資源延誤的這些時間可能會致使意想不到的行爲,或者可能須要你構建程序來規劃對這些資源的訪問。

  • 死鎖: 多線程之間互相等待對方釋放他們須要的資源/鎖,這組線程將永遠的被阻塞。

  • (線程)飢餓: 一個永遠沒法獲取資源,或者一組有特定的順序資源的線程,因爲各類緣由,它須要不斷嘗試去獲取他們卻永遠失敗。

  • 優先級反轉: 具備低優先級的線程持續獲取高優先級線程所需的資源,實質地反轉了系統指定的優先級。

  • 非決定論與公平性: 咱們沒法對線程獲取資源的時間和順序作出臆斷,這個延遲沒法事前肯定,並且它嚴重的受到線程間爭奪的影響,線程甚至從不能得到一個資源。可是用於守護關鍵部分的併發原語也能夠用來構建公平(fair)或者支持公平(fairness),確保全部等待的線程都可以訪問關鍵部分,而且遵照請求順序。

語言守護

即便在 Swift 語言自己沒有併發性相關功能的時期,它仍然提供了一些有關如何訪問屬性的保證。

例如全局變量的初始化是原子性地,咱們從不須要手動處理多個線程初始化同一個全局變量的併發狀況,或者擔憂初始化還在進行的過程當中看到一個只初始化了一部分的變量。

在下次討論單例的實現時,咱們會繼續討論這個特性。

但要記住的重要一點是,延遲屬性的初始化並非原子執行的,如今版本的語言並無提供註釋或修飾符來改變這一行爲。

類屬性的訪問也不是原子的,若是你須要訪問,那你不得不實現手動獨佔式的訪問,使用鎖或相似的機制。

線程

Foundation 提供了 Thread 類,內部基於 pthread,能夠用來建立新的線程並執行閉包。

線程可使用 Thread 類中的 detachNewThreadSelector:toTarget:withObject: 函數來建立,或者咱們能夠建立一個新的線程,聲明一個自定義的 Thread 類,而後覆蓋 main() 函數:

classMyThread : Thread {
    override func main(){
        print("Thread started, sleep for 2 seconds...")
        sleep(2)
        print("Done sleeping, exiting thread")
    }
}複製代碼

可是自從 iOS 10 和 macOS Sierra 推出之後,全部平臺終於可使用初始化指定執行閉包的方式建立線程,本文中全部的例子仍會擴展基礎的 Thread 類,這樣你就不用擔憂爲操做系統而作嘗試了。

var t = Thread {
    print("Started!")
}

t.stackSize = 1024 * 16
t.start()               //Time needed to spawn a thread around 100us複製代碼

一旦咱們有了一個線程實例,咱們須要手動的啓動它。做爲一個可選步驟,咱們也能夠爲線程定義棧的大小。

線程能夠經過調用 exit() 來緊急中止,可是咱們從不推薦這麼作,由於它不會給你機會來乾淨利落地終止當前任務,若是你有須要,多數狀況下你會選擇本身實現終止邏輯,或者只須要使用 cancel() 函數,而後檢查在主閉包中的 isCancelled 屬性,以明確線程是否須要在它天然結束以前終止當前的工做。

同步原語

當咱們有多個線程想要修改共享數據時,就頗有必要經過一些方式來處理這些線程之間的同步,防止數據破損和非肯定性行爲。

一般,用於同步線程的基本套路是鎖、信號量和監視器。

這些 Foundation 都提供了。

正如你要看到的,在 Swift 3 中,這些沒有去掉 NS 前綴的類(對,他們都是引用類型)實現了這些結構,可是在 Swift 接下來的某個版本中也許會去掉。

NSLock

NSLock 是 Foundation 提供的基本類型的鎖。

當一個線程嘗試鎖定一個對象時,可能會發生兩件事,若是鎖沒有被前面的線程獲取,那麼當前線程將獲得鎖並執行,不然線程將會陷入等待,阻塞執行,直到鎖的持有者解鎖它。換句話說,在同一時間,鎖是一種只能被一個線程獲取(鎖定)的對象,這可讓他們完美的監控對關鍵部分的訪問。

NSLock 和 Foundation 的其餘鎖都是不公平的,意思是,當一系列線程在等待獲取一個鎖時,他們不會按照他們原來的鎖定順序來獲取它。

你沒法預估執行順序。在線程爭奪的狀況下,當多個線程嘗試獲取資源時,有的線程可能會陷入飢餓,他們永遠也不會得到他們等待的鎖(或者不能及時的得到)。

沒有競爭地獲取鎖所須要的時間,測量在 100 納秒之內。可是在多個線程嘗試獲取鎖定的資源時,這個時間會急速增加。因此,從性能的角度來說,鎖並非處理資源分配的最佳方案。

讓咱們來看一個例子,例中有兩個線程,記住因爲鎖會被誰獲取的順序沒法肯定,T1 連續獲取兩次鎖的機會也會發生(可是不怎麼常見)。

let lock = NSLock()

class LThread : Thread {
    varid:Int = 0

    convenience init(id:Int){
        self.init()
        self.id = id
    }

    override func main(){
        lock.lock()
        print(String(id)+" acquired lock.")
        lock.unlock()
        iflock.try() {
            print(String(id)+" acquired lock again.")
            lock.unlock()
        }else{  // If already lockedmove along.
            print(String(id)+" couldn't acquire lock.")
        }
        print(String(id)+" exiting.")
    }
}

var t1 = LThread(id:1)
var t2 = LThread(id:2)
t1.start()
t2.start()複製代碼

在你決定使用鎖以前,容我多說一句。因爲你早晚會調試併發問題,要把鎖的使用,限制在某種數據結構的範圍內,而不是在代碼庫中的多個地方直接使用。

在調試併發問題的同時,檢查有少許入口的同步數據結構的狀態,比跟蹤某個部分的代碼處於鎖定,而且還要記住多個功能的本地狀態的方式更好。這會讓你的代碼走的更遠並讓你的併發結構更優雅。

NSRecursiveLock

遞歸鎖能被已經持有鎖的線程屢次獲取,在遞歸函數或者屢次調用檢查相同鎖的函數時頗有用處。不適用於基本的 NSLock。

let rlock = NSRecursiveLock()

classRThread : Thread {

    override func main(){
        rlock.lock()
        print("Thread acquired lock")
        callMe()
        rlock.unlock()
        print("Exiting main")
    }

    func callMe(){
        rlock.lock()
        print("Thread acquired lock")
        rlock.unlock()
        print("Exiting callMe")
    }
}

var tr = RThread()
tr.start()複製代碼

NSConditionLock

條件鎖提供了能夠獨立於彼此的附加鎖,用來支持更加複雜的鎖定設置(好比生產者-消費者的場景)。

一個全局鎖(不管特定條件如何都鎖定)也是可用的,而且行爲和經典的 NSLock 類似。

讓咱們看一個保護共享整數鎖的簡單的例子,每次生產者更新而消費者打印都會在屏幕上顯示。

let NO_DATA = 1
let GOT_DATA = 2

let clock = NSConditionLock(condition: NO_DATA)
var SharedInt = 0

classProducerThread : Thread {

    override func main(){
        for i in 0..<5 {="" clock.lock(whencondition:="" no_data)="" acquire="" the="" lock="" when="" no_data="" if="" we="" don't="" have="" to="" wait="" for="" consumers="" could="" just="" done="" clock.lock()="" sharedint="i" clock.unlock(withcondition:="" got_data)="" unlock="" and="" set="" as="" got_data="" }="" classconsumerthread="" :="" thread="" override="" func="" main(){="" i="" in0..<5="" print(i)="" let="" pt="ProducerThread()" ct="ConsumerThread()" ct.start()="" pt.start()<="" code="">
  
  
  

 複製代碼

當建立鎖的時候,咱們須要指定一個由整數表明的初始條件。

lock(whenCondition:) 函數在條件符合時會得到鎖,或者等待另外一個線程用 unlock(withCondition:) 設置值來釋放鎖定。

對比基本鎖的一個小改進是,咱們能夠對更復雜的場景進行稍微建模。

NSCondition

不要與條件鎖產生混淆,一個條件提供了一種乾淨的方式來等待條件的發生。

當獲取了鎖的線程驗證它須要的附加條件(一些資源,處於特定狀態的另外一個對象等等)不能知足時,它須要一種方式被擱置,一旦知足條件再繼續它的工做。

這能夠經過連續性或週期性地檢查這種條件(繁忙等待)來實現,可是這麼作,線程持有的鎖會發生什麼?在咱們等待的時候是保持仍是釋放他們以致於在條件符合時從新獲取他們?

條件提供了一個乾淨的方式來解決這個問題,一旦獲取一個線程,就把它放進關於這個條件的一個等待列表中,它會在另外一個線程發信號時,表示條件知足,而被喚醒。

讓咱們看個例子:

let cond = NSCondition()
var available = false
var SharedString = ""
classWriterThread : Thread {

    override func main(){
        for _ in0..<5 5="" {="" cond.lock()="" sharedstring="😅" available="true" cond.signal()="" notify="" and="" wake="" up="" the="" waiting="" thread="" s="" cond.unlock()="" }="" classprinterthread="" :="" override="" func="" main(){="" for="" _="" in0..<5="" just="" do="" it="" times="" while(!available){="" protect="" from="" spurious="" signals="" cond.wait()="" print(sharedstring)="" let="" writet="WriterThread()" printt="PrinterThread()" printt.start()="" writet.start()<="" code="">
  
  
  

 複製代碼

NSDistributedLock

分佈式鎖與以前咱們所看到的大相徑庭,我不指望你常常須要它們。

它們由多個應用程序共享,並由文件系統上的條目(如簡單文件)支持。很明顯這個文件系統能被全部想要獲取他(分佈式鎖)的應用訪問。

這種鎖須要使用 try() 函數,一個非阻塞方法,它當即返回一個布爾值,指出是否獲取鎖。獲取鎖一般須要屢次的手動執行,並在連續嘗試之間適當延遲。

分佈式鎖一般使用 unlock() 方法釋放。

讓咱們看一個基本的例子:

var dlock = NSDistributedLock(path: "/tmp/MYAPP.lock")

iflet dlock = dlock {
    var acquired = falsewhile(!acquired){
        print("Trying to acquire the lock...")
        usleep(1000)
        acquired = dlock.try()
    }

    // Do something...

    dlock.unlock()
}複製代碼

OSAtomic 你在哪裏?

OSAtomic 提供的原子操做是簡單的,而且容許設置、獲取或比較變量,而不須要經典的鎖邏輯,由於他們利用 CPU 的特定功能(有時是原生原子指令),並提供了比前面鎖所描述的更優越的性能。

對於創建併發數據結構來說,他們是很是有用的,由於處理併發所需的開銷被下降到最低。

OSAtomic 在 macOS 10.12 已經被捨棄使用,而在 Linux 上歷來都不可使用,可是一些開源的的項目,好比這個提供了實用的 Swift 擴展,或者這個提供了相似的功能。

同步塊

在 Swift 中你不能像在 Objective-C 中同樣,建立一個 @synchronized 塊,由於沒有等效的關鍵字可用。

在 Darwin 上,經過一些代碼,你能夠直接使用 objc_sync_enter(OBJ)objc_sync_exit(OBJ) 來弄出相似的東西,以進入現有的 @objc 對象監視器,就像 @synchronized 在底層所作的同樣,但這並不值得,若是你想要他們更靈活的話,最好是簡單地使用一個鎖。

就如咱們將要描述調度隊列時看到的,用隊列,咱們甚至可使用更少的代碼來執行同步調用來複制這個功能:

var count: Int {
    queue.sync {self.count}
}複製代碼

本文或其餘文章的 playground 能夠在 GitHubZipped 找到。

GCD: 大中樞派發

對於不熟悉這個 API 的人來講,GCD 是一種基於隊列的 API,容許在工做池上執行閉包。

換句話說,包含須要執行的工做的閉包能被添加到一個隊列中,隊列會依賴於配置選項,順序或並行的用一系列線程來執行他們。可是不管隊列是什麼類型的,工做始終會按照先進先出的順序啓動,這意味着工做會始終遵循插入順序啓動。完成順序將依賴於每項工做的持續時間。

這是一種常見的模式,幾乎能夠從每一個處理併發的相對現代的語言運行時系統中找到。線程池的方式比一系列空閒和無關的線程更易於管理、檢查和控制。

GCD 的 API 在 Swift 3 中有一些小改動,SE-0088 模塊化了它的設計,讓它看上去更面向對象了。

調度隊列

GCD 容許建立自定義的隊列,可是也提供了一些能夠訪問的預約義系統隊列。

要建立一個順序執行你的閉包的基本串行隊列,你只須要提供一個字符串標籤來標識它,一般建議使用反向域名前綴,在堆棧追蹤的時候就能簡單地跟蹤隊列的全部者。

let serialQueue = DispatchQueue(label: "com.uraimo.Serial1")  //attributes: .serial

let concurrentQueue = DispatchQueue(label: "com.uraimo.Concurrent1", attributes: .concurrent)複製代碼

咱們建立的第二個隊列是併發的,意味着在執行工做時,隊列會使用底層線程池中的全部可用線程。這種狀況下,執行順序是沒法預測的,不要覺得你的閉包完成的順序與插入順序有任何關係。

能夠從 DispatchQueue 對象得到默認隊列:

let mainQueue = DispatchQueue.main

let globalDefault = DispatchQueue.global()複製代碼

main 隊列是 iOS 和 macOS 上處理圖形應用主事件循環的順序主隊列,用於響應事件和更新用戶界面。就如咱們知道的,每一個對用戶界面的改動都會在這個隊列執行,且這個線程中任何一個耗時操做都會使用戶界面的渲染變得不及時。

運行時系統也提供了對其餘不一樣優先級全局隊列的訪問,能夠經過 Quality of Service (Qos) 參數來查看他們的標識。

不一樣優先級聲明在 DispatchQoS 類裏,優先級從高到低:

  • .userInteractive
  • .userInitiated
  • .default
  • .utility
  • .background
  • .unspecified

重要的是要注意,移動設備提供了低電量模式,在電池較低時,後臺隊列會掛起

要取得一個特定的默認全局隊列,使用 global(qos:) 根據想要的優先級來獲取:

let backgroundQueue = DispatchQueue.global(qos: .background)複製代碼

在建立自定義隊列時,也能夠選擇使用與其餘屬性相同的優先說明符:

let serialQueueHighPriority = DispatchQueue(label: "com.uraimo.SerialH", qos: .userInteractive)複製代碼

使用隊列

包含任務的閉包能夠以兩種方式提交給隊列:同步異步,分別使用 syncasync 方法。

在使用前者時,sync 會被阻塞,換句話說,當它閉包完成(在你須要等待閉包完成時頗有用,可是有更好的途徑)時調用的 sync 方法纔會完成,然後者會把閉包添加到隊列,而後容許程序繼續執行。

讓咱們看一個簡單的例子:

globalDefault.async {
    print("Async on MainQ, first?")
}

globalDefault.sync {
    print("Sync in MainQ, second?")
}複製代碼

多個調度能夠嵌套,例如在後臺完成一些東西、低優先、須要咱們更新用戶界面的操做。

DispatchQueue.global(qos: .background).async {
    // Some background work here

    DispatchQueue.main.async {
        // It's time to update the UI
        print("UI updated on main queue")
    }
}複製代碼

閉包也能夠在一個特定的延遲以後執行,Swift 3 最終以一種更溫馨的方式指定這個時間間隔,那就是使用 DispatchTimeInterval 工具枚舉,它容許使用這四個時間單位組成間隔:.seconds(Int).milliseconds(Int).microseconds(Int).nanoseconds(Int)

要安排一個閉包在未來執行,使用 asyncAfter(deadline:execute:) 方法,並傳遞一個時間:

globalDefault.asyncAfter(deadline: .now() + .seconds(5)) {
    print("After 5 seconds")
}複製代碼

若是你須要屢次併發執行相同的閉包(就像你之前用 dispatch_apply 同樣),你可使用 concurrentPerform(iterations:execute:) 方法,但請注意,若是在當前隊列的上下文中可能的話,這些閉包會併發執行,因此記得,始終應該在支持併發的隊列中同步或異步地調用此方法。

globalDefault.sync {  
    DispatchQueue.concurrentPerform(iterations: 5) {
        print("\($0) times")
    }
}複製代碼

雖然隊列在一般狀況下,建立好就會準備執行它的閉包,可是它也能夠配置爲按需啓動。

let inactiveQueue = DispatchQueue(label: "com.uraimo.inactiveQueue", attributes: [.concurrent, .initiallyInactive])
inactiveQueue.async {
    print("Done!")
}

print("Not yet...")
inactiveQueue.activate()
print("Gone!")複製代碼

這是咱們第一次須要制定多個屬性,但就如你所見,若是須要,你能夠用一個數組添加多個屬性。

也可使用繼承自 DispatchObject 的方法暫停或恢復執行的工做:

inactiveQueue.suspend()

inactiveQueue.resume()複製代碼

僅用於配置非活動隊列(在活動的隊列中使用會形成崩潰)優先級的方法 setTarget(queue:) 也是可用的。調用此方法的結果是將隊列的優先級設置爲與給定參數的隊列相同的優先級。

屏障

讓咱們假設你添加了一組閉包到特定的隊列(執行閉包的持續時間不一樣),可是如今你想只有當全部以前的異步任務完成時再執行一個工做,你可使用屏障來作這樣的事情。

讓咱們添加五個任務(會睡眠 1 到 5 秒的時間)到咱們前面建立的併發隊列中,一旦其餘工做完成,就利用屏障來打印一些東西,咱們在最後 async 的調用中規定一個 DispatchWorkItemFlags.barrier 標誌來作這件事。

globalDefault.sync { 
    DispatchQueue.concurrentPerform(iterations: 5) { (id:Int) in
        sleep(UInt32(id)+1)
        print("Async on globalDefault, 5 times: "+String(id))
    }
}   

globalDefault.async (flags: .barrier) {
    print("All 5 concurrent tasks completed")
}複製代碼

單例和 Dispatch_once

就如你所知的同樣,在 Swift 3 中並無與 dispatch_once 等效的函數,它多數用來構建線程安全的單例。

幸運地,Swift 保證了全局變量的初始化是原子性地,若是你認爲常量在初始化後,他們的值不能發生改變,這兩個屬性使全局常量成爲實現單例的更容易的選擇。

final classSingleton {

    public static let sharedInstance: Singleton = Singleton()

    privateinit() { }

    ...
}複製代碼

咱們將類聲明爲 final 以拒絕它子類化的能力,咱們把它的指定構造器設爲私有,這樣就不能手動建立它對象的實例。公共靜態變量是進入單例的惟一入口,它會用於獲取單例、共享實例。

相同的行爲能夠用於定義只執行一次的代碼塊:

func runMe() {
    struct Inner {
        static let i: () = {
            print("Once!")
        }()
    }
    Inner.i
}

runMe()
runMe() // Constant already initialized
runMe() // Constant already initialized複製代碼

雖然不太好看,可是它的確能夠正常工做,並且若是隻是執行一次,它也是能夠接受的實現。

可是若是咱們須要徹底的複製 dispatch_once 的功能,咱們就須要從頭實現它,就如同步塊中描述的同樣,利用一個擴展:

import Foundation

public extension DispatchQueue {

    private static var onceTokens = [Int]()
    private static var internalQueue = DispatchQueue(label: "dispatchqueue.once")

    public class func once(token: Int, closure: (Void)->Void) {
        internalQueue.sync {
            if onceTokens.contains(token) {
                return
            }else{
                onceTokens.append(token)
            }
            closure()
        }
    }
}

let t = 1
DispatchQueue.once(token: t) {
    print("only once!")
}
DispatchQueue.once(token: t) {
    print("Two times!?")
}
DispatchQueue.once(token: t) {
    print("Three times!!?")
}複製代碼

和預期一致,三個閉包中,只有第一個會被實際執行。

或者,可使用 objc_sync_enterobjc_sync_exit 來構建性能稍微好一點的東西,若是他們在你的平臺上可用的話:

import Foundation

public extension DispatchQueue {

    privatestatic var _onceTokens = [Int]()

    publicclass func once(token: Int, closure: (Void)->Void) {
        objc_sync_enter(self);
        defer { objc_sync_exit(self) }

        if _onceTokens.contains(token) {
            return
        }else{
            _onceTokens.append(token)
        }
        closure()
    }
}複製代碼

Dispatch Groups

若是你有多個任務,雖然把他們添加到不一樣的隊列,也但願等待他們的任務完成,你能夠把他們分到一個派發組中。

讓咱們看一個例子,任務直接被添加到一個特定的組,用 syncasync 調用:

let mygroup = DispatchGroup()

for i in0..<5 {="" globaldefault.async(group:="" mygroup){="" sleep(uint32(i))="" print("group="" async on="" globaldefault:"+string(i))="" }="" }<="" code="">
  
  
  

 複製代碼

任務在 globalDefault 上執行,可是咱們能夠註冊一個 mygroup 完成的處理程序,咱們能夠選擇在全部這些被完成後,執行這個隊列中的閉包。wait() 方法能夠用於執行一個阻塞等待。

print("Waitingforcompletion...")
mygroup.notify(queue: globalDefault) {
    print("Notify received, done waiting.")
}
mygroup.wait()
print("Done waiting.")複製代碼

另外一種追蹤隊列任務的方式是,在隊列執行調用的時候,手動的進入和離開一個組,而不是直接指定它:

for i in 0..<5 {="" mygroup.enter()="" sleep(uint32(i))="" print("group="" sync="" on="" mainq:"+string(i))="" mygroup.leave()="" }<="" code="">
  
  
  

 複製代碼

Dispatch Work Items

閉包不是指定做業須要由隊列執行的惟一方法,有時你可能須要一個可以跟蹤其執行狀態的容器類型,爲此,咱們就有 DispatchWorkItem。每一個接受閉包的方法都有一個工做項的變型。

工做項封裝一個由隊列的線程池調用 perform() 方法執行的閉包:

let workItem = DispatchWorkItem {
    print("Done!")
}

workItem.perform()複製代碼

WorkItems 也提供其餘頗有用的方法,好比 notify,與組同樣,容許在一個指定的隊列完成時執行一個閉包

workItem.notify(queue: DispatchQueue.main) {
    print("Notify on Main Queue!")
}

defaultQueue.async(execute: workItem)複製代碼

咱們也能夠等到閉包已經被執行或者在隊列嘗試執行它以前,使用 cancel() 方法(在閉包執行之間不會取消執行)把它標記爲移除。

print("Waiting for work item...")
workItem.wait()
print("Done waiting.")

workItem.cancel()複製代碼

可是,重要的是要知道,wait() 不只僅會阻塞當前線程的完成,也會提高隊列中全部前面的工做項目的優先級,以便於儘快的完成這個特定的項目。

Dispatch Semaphores

Dispatch Semaphores 是一種由多個線程獲取的鎖,它依賴於計數器的當前值。

線程在信號量上 wait,直到那個每當信號量被獲取時值都減少的計數器的值爲 0

用於訪問信號量,釋放等待線程的插槽名爲 signal,它可讓計數器的計數增長。

讓咱們看一個簡單的例子:

let sem = DispatchSemaphore(value: 2)

// The semaphore will be held by groups of two pool threads
globalDefault.sync {
    DispatchQueue.concurrentPerform(iterations: 10) { (id:Int) in
        sem.wait(timeout: DispatchTime.distantFuture)
        sleep(1)
        print(String(id)+" acquired semaphore.")
        sem.signal()
    }
}複製代碼

Dispatch Assertions

Swift 3 介紹了一種新的函數來執行當前上下文的斷言,能夠校驗閉包是否在指望的隊列上執行。咱們可使用 DispatchPredicate 的三個枚舉來構建謂詞:.onQueue,用來校驗在特定的隊列,.notOnQueue,來校驗相反的狀況,以及 .onQueueAsBarrier,來校驗是否當前的閉包或工做項是隊列上的一個障礙。

dispatchPrecondition(condition: .notOnQueue(mainQueue))
dispatchPrecondition(condition: .onQueue(queue))複製代碼

本文或其餘文章的 playground 能夠在 GitHubZipped 找到。

Dispatch Sources

Dispatch Sources 是處理系統級別異步事件(好比內核信號或系統,文件套接字相關事件)的一種便捷方式。

有幾種可用的調度源,分組以下:

  • Timer Dispatch Sources: 用於在特定時間點或週期性事件中生成事件 (DispatchSourceTimer)。
  • Signal Dispatch Sources: 用於處理 UNIX 信號 (DispatchSourceSignal)。
  • Memory Dispatch Sources: 用於註冊與內存使用狀態相關的通知 (DispatchSourceMemoryPressure)。
  • Descriptor Dispatch Sources: 用於註冊與文件和套接字相關的不一樣事件 (DispatchSourceFileSystemObject, DispatchSourceRead, DispatchSourceWrite)。
  • Process dispatch sources: 用於監視與執行狀態有關的某些事件的外部進程 (DispatchSourceProcess)。
  • Mach related dispatch sources: 用於處理與Mach內核的 IPC 設備有關的事件 (DispatchSourceMachReceive, DispatchSourceMachSend)。

若是有須要,你也能夠構建你本身的調度源。全部調度源都符合 DispatchSourceProtocol 協議,它定義了註冊處理程序所需的基本操做,並修改了調度源的激活狀態。

讓咱們經過一個 DispatchSourceTimer 相關的例子,來理解如何使用這些對象。

源是由 DispatchSource 提供的工具方法建立的,在這咱們會使用 makeTimerSource,指定咱們想要執行處理程序的調度隊列。

Timer Sources 沒有其餘的參數,因此咱們只須要指定隊列,建立源,就如咱們所見,可以處理多個事件的調度源一般須要你指定要處理的事件的標識符。

let t = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
t.setEventHandler{ print("!") }
t.scheduleOneshot(deadline: .now() + .seconds(5), leeway: .nanoseconds(0))
t.activate()複製代碼

一旦源被建立,咱們就會使用 setEventHandler(closure:) 註冊一個事件處理程序,若是不須要其餘配置,就能夠經過 activate() 讓源可用。

調度源初始化不具有活性,意味着若是沒有進一步的配置,他們不會開始傳遞事件。一旦咱們準備就緒,源就能經過 activate() 激活,若是有須要,能夠經過 suspend()resume() 來暫時掛起和恢復事件傳遞。

Timer Sources 須要一個額外的步驟來配置對象須要傳遞的是哪種類型的定時事件。在上面的例子中,咱們定義了單一的事件,會在註冊後 5 秒嚴格執行。

咱們也能夠配置對象來傳遞週期性事件,就像咱們使用 Timer 對象那樣:

t.scheduleRepeating(deadline: .now(), interval: .seconds(5), leeway: .seconds(1))複製代碼

當咱們完成了調度源的使用,並想要徹底中止事件的傳遞時,咱們能夠調用 cancel(),它會中止事件源,調用消除相關的處理程序(若是咱們已經設置了一個處理一些結束後的清理操做,好比註銷)。

t.cancel()複製代碼

對於其餘類型的調度源來講 API 都是類似的,讓咱們看一個關於 Kitura 初始化讀取源的例子,它用於在已創建的套接字上進行異步讀取:

readerSource = DispatchSource.makeReadSource(fileDescriptor: socket.socketfd,
                                             queue: socketReaderQueue(fd: socket.socketfd))

readerSource.setEventHandler() {
    _ = self.handleRead()
}
readerSource.setCancelHandler(handler: self.handleCancel)
readerSource.resume()複製代碼

當套接字的數據緩衝區有新的字節能夠傳入的時候,handleRead() 方法會被調用。Kitura 也使用 WriteSource 執行緩衝寫入,使用調度源事件有效地調整寫入速度,一旦套接字通道準備好發送就寫入新的字節。在執行 I/O 操做的時候,對比於 Unix 平臺上的其餘低階 API,讀/寫源是一個很好的高階替代。

與文件相關的調度源的主題,另外一個在某些狀況中可能有用的是 DispatchSourceFileSystemObject,它容許監聽特定文件的更改,從其名稱到其屬性。經過此調度源,在文件被修改或刪除時,你也會收到通知。Linux 上的事件子集實質上都是由 inotify 內核子系統管理的。

剩餘類型的源操做大同小異,你能夠從 libDispatch 的文檔中查看完整的列表,可是記住他們其中的一些,好比 Mach 源和內存壓力源只會在 Darwin 的平臺工做。

操做與可操做的隊列

咱們簡要的介紹一下 Operation Queues,以及創建在 GCD 之上的附加 API。它們使用併發隊列和模型任務做爲操做,這樣作能夠輕鬆的取消操做,並且能讓他們的執行依賴於其餘操做的完成。

操做能定義一個執行順序的優先級,被添加到 OperationQueues裏異步執行。

咱們看一個基礎的例子:

var queue = OperationQueue()
queue.name = "My Custom Queue"queue.maxConcurrentOperationCount = 2

var mainqueue = OperationQueue.main //Refers to the queue of the main threadqueue.addOperation{
    print("Op1")
}
queue.addOperation{
    print("Op2")
}複製代碼

咱們也能夠建立一個阻塞操做對象,而後在加入隊列以前配置它,若有須要,咱們也能夠向這種操做添加多個閉包。

要注意的是,在 Swift 中不容許 NSInvocationOperation 使用目標+選擇器建立操做。

var op3 = BlockOperation(block: {
    print("Op3")
})
op3.queuePriority = .veryHigh
op3.completionBlock = {
    if op3.isCancelled {
        print("Someone cancelled me.")
    }
    print("Completed Op3")
}

var op4 = BlockOperation {
    print("Op4 always after Op3")
    OperationQueue.main.addOperation{
        print("I'm on main queue!")
    }
}複製代碼

操做能夠有主次優先級,一旦主優先級完成,次優先級纔會執行。

咱們能夠從 op4 添加一個依賴關係到 op3,這樣 op4 會等待 op3 的完成再執行。

op4.addDependency(op3)

queue.addOperation(op4)  // op3 will complete before op4, alwaysqueue.addOperation(op3)複製代碼

依賴也能夠經過 removeDependency(operation:) 移除,被存儲到一個公共可訪問的 dependencies 數組裏。

當前操做的狀態能夠經過特定的屬性查看:

op3.isReady       //Ready for execution?
op3.isExecuting   //Executing now?
op3.isFinished    //Finished naturally or cancelled?
op3.isCancelled    //Manually cancelled?複製代碼

你能夠調用 cancelAllOperations 方法,取消隊列中全部的當前操做,這個方法會設置隊列中剩餘操做的 isCancelled 屬性。一個單獨的操做能夠經過調用它的 cancel 方法來取消:

queue.cancelAllOperations() 

op3.cancel()複製代碼

若是在計劃運行隊列以後取消操做,建議您檢查操做中的 isCancelled 屬性,跳過執行。

最後要說是,你也能夠中止操做隊列上執行新的操做(正在執行的操做不會受到影響):

queue.isSuspended = true複製代碼

本文或其餘文章的 playground 能夠在 GitHubZipped 找到。

閉幕後的思考

本文能夠說是從 Swift 可用的外部併發框架的視角,給出一個很好的總結。

第二部分將重點介紹下一步可能在語言中出現的處理併發的「原生」功能,而不須要藉助外部庫。經過目前的一些開源實現來說述幾個有意思的範例。

我但願這兩篇文章可以對併發世界作一個很好的介紹,而且將幫助你瞭解和參與在急速發展的郵件列表中的討論,在社區開始考慮將要介紹的內容時,咱們一塊兒期待 Swift 5 的到來。

關於併發和 Swift 的更多有趣內容,請看 Cocoa With Love 的博客。

你喜歡這篇文章嗎?讓我在推特上看到你!


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索