做者:Federico Zanetello 翻譯:BigNerdCoding,原文連接編程
首先,若是你對大中樞派發(GCD)和派發隊列不夠熟悉的話,請先看 AppCoda 的這篇文章。swift
在瞭解了 GCD 內容後,接下來咱們來看看 Swift 中的信號機制。安全
先讓咱們假象一個場景:有一羣做者在寫做的時候必須共享一支筆來完成我的的工做。很明顯在這種情形下,每次都只能有一我的可以進行寫做。多線程
在代碼世界中,上述場景中寫做者就至關於線程而筆就是須要共享的資源(例如:文件、變量、某種權限)。併發
那麼問題來了,如何保證這些共享資源的互斥使用?app
對於這些資源的互斥使用問題有人可能會想,只需一個 Bool 類型的 resourceIsAvailable 變量就足夠了:async
if (resourceIsAvailable) { resourceIsAvailable = false useResource() resourceIsAvailable = true } else { // resource is not available, wait or do something else }
可是在多線程併發的狀況下(不考慮優先級),咱們是沒法得知具體是哪一個線程在執行上述代碼。ui
例如,如今有兩個線程 threadA、threadB 都要執行上面的代碼,而且對資源的使用是互斥的。那麼就可能出現如下情形:spa
因此,不使用 GCD 就想完成線程安全代碼的編寫是一件很是困難的事情。線程
簡單來講,分爲三個步驟:
當共享資源只有一份而且只能被一個線程佔有的時候,那麼你能夠將上面的 request/signal 理解爲對資源的 lock/unlock。
首先信號機制須要一個信號量來控制訪問權限,它的組成以下:
當信號機制接受到請求後,它會先去檢查本身的資源計數是否大於 0:
當信號機制收到一個使用完畢的釋放消息時,他會先去檢查請求隊列:
當線程向信號機制請求資源分配可是沒有獲得知足時,該線程將會被凍結直到成功獲取了資源的使用權。
⚠️ 若是該線程是主線程的話,那麼整個 App 都將會被凍結失去響應。
說了那麼多,下面咱們經過代碼來更好的理解該機制。
信號量結構的聲明很是的簡單:
let semaphore = DispatchSemaphore(value: 1)
其中的參數 value,表示了可供使用的資源總數。
請求資源分配也很是的簡單:
semaphore.wait()
須要注意的是,該信號量並無給予線程任何物理資,僅僅只是一個使用權限。線程只能在 request 和 release 操做之間對資源進行使用。
一旦線程得到了訪問權限,那麼咱們就能夠假定線程必定可以對資源進行正常操做。
在釋放資源的時候,咱們這樣寫:
semaphore.signal()
當完成資源釋放後,該線程就沒法使用該資源了,除非它再次發起使用請求。
與AppCoda 的這篇文章同樣,接下來咱們看看信號機制的真實使用場景。
由於 Swift Playgrounds 並不能完美支持,因此這裏咱們使用的是 Xcode Playgrounds。但願 WWDC17 中蘋果可以對 Swift Playgrounds 進行功能提高吧。
在下面的 playgrounds 中會建立兩個線程而且二者將賦予不一樣的優先級,而後執行的時候打印十次 emoji。
import Foundation import PlaygroundSupport let higherPriority = DispatchQueue.global(qos: .userInitiated) let lowerPriority = DispatchQueue.global(qos: .utility) func asyncPrint(queue: DispatchQueue, symbol: String) { queue.async { for i in 0...10 { print(symbol, i) } } } asyncPrint(queue: higherPriority, symbol: "?") asyncPrint(queue: lowerPriority, symbol: "?") PlaygroundPage.current.needsIndefiniteExecution = true
正如預料中的那樣,高優先級的線程早於低優先級的線程結束任務:
接下來咱們對上面的代碼進行改寫,在其中加入信號機制。爲此咱們須要定義一個信號量並對其中的 asyncPrint 進行修改:
import Foundation import PlaygroundSupport let higherPriority = DispatchQueue.global(qos: .userInitiated) let lowerPriority = DispatchQueue.global(qos: .utility) let semaphore = DispatchSemaphore(value: 1) func asyncPrint(queue: DispatchQueue, symbol: String) { queue.async { print("\(symbol) waiting") semaphore.wait() // requesting the resource for i in 0...10 { print(symbol, i) } print("\(symbol) signal") semaphore.signal() // releasing the resource } } asyncPrint(queue: higherPriority, symbol: "?") asyncPrint(queue: lowerPriority, symbol: "?") PlaygroundPage.current.needsIndefiniteExecution = true
爲了查看每一個線程在執行時的真實狀態,咱們在代碼中打印了更多的信息。
如上所示,當你開始打印出某個線程的執行狀態的時候,另外一個線程必須等待前者執行結束才能獲得執行。無論第二個進程時於什麼時候發送了 wait() 請求,它都必須等待第一個進程的執行結束釋放資源。
如今咱們知道了信號機制是如何工做的,接下來咱們檢查下面的打印信息:
圖示的情形是由於在執行上面的代碼時,處理器優先選擇了低優先級的進程。當這種情形發生的時候高優先級的進程也必須等待資源的釋放。這種情形在代碼中徹底有可能發生,而這種情形在編程世界中被稱爲優先級反轉。
在與信號量機制不一樣的其餘編程概念中,上述情形發生後低優先級的線程會暫時繼承全部等待進程中的優先級最高進程的優先級,這被稱爲優先級繼承。
如今設想一個更糟糕的狀況,在當前最高和最低優先級線程中間還存在大量的默認優先級線程。在上面的優先級反轉的情形下,高優先級線程排在低優先級線程以後,可是與此同時大量的默認優先級又有可能排在低優先級線程以前(畢竟優先級高)。
這種可能的情況出現後,就會致使高優先級線程長時間處於飢餓的等待狀態。
在我看來,信號機制應該在全部資源競爭線程的優先級相同的情形下使用。若是不知足該條件的話,我建議你看看 Regions 和 Monitors。
下面的示例中將有兩個線程都須要獨佔資源 A、B 的使用權。
若是兩個資源能夠單獨使用,則爲每一個資源定義一個信號量是有意義的。 若是不行的話則使用一個信號量進行管理。
在前一種狀況(2資源,2信號量)下可能出現以下狀況:高優先級線程將使用第一個資源「A」,而後是「B」,而咱們的低優先級線程將使用第一個資源「B」 而後是「A」。
代碼以下:
import Foundation import PlaygroundSupport let higherPriority = DispatchQueue.global(qos: .userInitiated) let lowerPriority = DispatchQueue.global(qos: .utility) let semaphoreA = DispatchSemaphore(value: 1) let semaphoreB = DispatchSemaphore(value: 1) func asyncPrint(queue: DispatchQueue, symbol: String, firstResource: String, firstSemaphore: DispatchSemaphore, secondResource: String, secondSemaphore: DispatchSemaphore) { func requestResource(_ resource: String, with semaphore: DispatchSemaphore) { print("\(symbol) waiting resource \(resource)") semaphore.wait() // requesting the resource } queue.async { requestResource(firstResource, with: firstSemaphore) for i in 0...10 { if i == 5 { requestResource(secondResource, with: secondSemaphore) } print(symbol, i) } print("\(symbol) releasing resources") firstSemaphore.signal() // releasing first resource secondSemaphore.signal() // releasing second resource } } asyncPrint(queue: higherPriority, symbol: "?", firstResource: "A", firstSemaphore: semaphoreA, secondResource: "B", secondSemaphore: semaphoreB) asyncPrint(queue: lowerPriority, symbol: "?", firstResource: "B", firstSemaphore: semaphoreB, secondResource: "A", secondSemaphore: semaphoreA) PlaygroundPage.current.needsIndefiniteExecution = true
若是幸運的話:
簡單來講,高優先級的線程獲得了兩個資源的使用權並在執行完成後低優先級的線程繼續執行。
可是,若是運氣很差的話:
兩個線程都沒法完成任務,他們都在等待對方釋放其手中的資源使用權。這就是計算機中死鎖概念。
實話說,死鎖在真實世界中是很難處理的一個問題。全部,咱們在一開始的時候就應該儘可能避免這種狀況的發生。例如上例中咱們能夠將兩個資源捆綁在一塊兒作爲一個信號量,雖然在效率上可能存在必定的犧牲。
另外,在一些系統中當發生死鎖時,系統會將其中某個線程幹掉來打破這種狀態。
或者你可使用 Ostrich algorithm。