Swift「信號」機制概述

做者:Federico Zanetello 翻譯:BigNerdCoding,原文連接編程

首先,若是你對大中樞派發(GCD)和派發隊列不夠熟悉的話,請先看 AppCoda 的這篇文章swift

在瞭解了 GCD 內容後,接下來咱們來看看 Swift 中的信號機制。安全

Cover

簡介

先讓咱們假象一個場景:有一羣做者在寫做的時候必須共享一支筆來完成我的的工做。很明顯在這種情形下,每次都只能有一我的可以進行寫做。多線程

在代碼世界中,上述場景中寫做者就至關於線程而就是須要共享的資源(例如:文件、變量、某種權限)。併發

那麼問題來了,如何保證這些共享資源的互斥使用?app

1*nfAYVSYFMB874-z4sfJ_YQ

共享資源的訪問控制

對於這些資源的互斥使用問題有人可能會想,只需一個 Bool 類型的 resourceIsAvailable 變量就足夠了:async

if (resourceIsAvailable) {
  resourceIsAvailable = false
  useResource()
  resourceIsAvailable = true
} else {
  // resource is not available, wait or do something else
}

可是在多線程併發的狀況下(不考慮優先級),咱們是沒法得知具體是哪一個線程在執行上述代碼。ui

示例

例如,如今有兩個線程 threadA、threadB 都要執行上面的代碼,而且對資源的使用是互斥的。那麼就可能出現如下情形:spa

  • threadA 首先執行了條件判斷語句,而且獲得的資源的訪問權限。
  • 可是在執行權限鎖定的代碼以前( resourceIsAvailable = false),處理器切換到 threadB 而且也執行了條件判斷語句。
  • 如今兩個線程都有了訪問權限,這就致使了很嚴重的問題。

因此,不使用 GCD 就想完成線程安全代碼的編寫是一件很是困難的事情。線程

1*p54pBislRafckGffcDqRdA

How Semaphores Work:

簡單來講,分爲三個步驟:

  1. 當你須要使用共享資源的時候,首先給型號機制發送權限請求(request)。
  2. 當信號機制對你開啓綠燈放行的時候,咱們就能夠確保當前資源已經可以被咱們使用。
  3. 當資源使用完畢後,你必須給信號機制發送通知(signal),讓它回收權限並再次分派給其餘線程。

當共享資源只有一份而且只能被一個線程佔有的時候,那麼你能夠將上面的 request/signal 理解爲對資源的 lock/unlock

1*-_owdkyNPRUQS5a5yjdEkA

幕後的運行機制

The Structure

首先信號機制須要一個信號量來控制訪問權限,它的組成以下:

  • 一個計數器 counter 用於標記可用資源數,也就是說它表示了當前還有多少資源還能被線程使用。
  • 一個 FIFO 的線程派發隊列,用於處理等待資源訪問權限的線程。

Resource Request:wait()

當信號機制接受到請求後,它會先去檢查本身的資源計數是否大於 0:

  • 若是大於0,則資源計數減 1 ,並將資源分配給請求者使用。
  • 若是不知足,則將該請求線程放到請求隊列的最後。

Resource Release:signal()

當信號機制收到一個使用完畢的釋放消息時,他會先去檢查請求隊列:

  • 若是請求隊列裏的線程不爲空的話,則將隊列中的第一個線程移出並將資源分配給該線程。
  • 不然則增長資源計數。

Warning: Busy Waiting

當線程向信號機制請求資源分配可是沒有獲得知足時,該線程將會被凍結直到成功獲取了資源的使用權。

⚠️ 若是該線程是主線程的話,那麼整個 App 都將會被凍結失去響應。

1*3GANzX3n1uEiuhXE49fcrg

信號機制在 Swift 中的使用

說了那麼多,下面咱們經過代碼來更好的理解該機制。

Declaration

信號量結構的聲明很是的簡單:

let semaphore = DispatchSemaphore(value: 1)

其中的參數 value,表示了可供使用的資源總數。

Resource Request

請求資源分配也很是的簡單:

semaphore.wait()

須要注意的是,該信號量並無給予線程任何物理資,僅僅只是一個使用權限。線程只能在 requestrelease 操做之間對資源進行使用。

一旦線程得到了訪問權限,那麼咱們就能夠假定線程必定可以對資源進行正常操做。

Resource Release

在釋放資源的時候,咱們這樣寫:

semaphore.signal()

當完成資源釋放後,該線程就沒法使用該資源了,除非它再次發起使用請求。

Semaphore Playgrounds

與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

正如預料中的那樣,高優先級的線程早於低優先級的線程結束任務:

1*OjtJO8-44tStXpRS8y1N-A

採用信號機制

接下來咱們對上面的代碼進行改寫,在其中加入信號機制。爲此咱們須要定義一個信號量並對其中的 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

爲了查看每一個線程在執行時的真實狀態,咱們在代碼中打印了更多的信息。

1*g7SMrR7svWNetOqjSGIEYA

如上所示,當你開始打印出某個線程的執行狀態的時候,另外一個線程必須等待前者執行結束才能獲得執行。無論第二個進程時於什麼時候發送了 wait() 請求,它都必須等待第一個進程的執行結束釋放資源。

優先級逆轉

如今咱們知道了信號機制是如何工做的,接下來咱們檢查下面的打印信息:

1*eCFBl9XpF6JYX1b8xwD26

圖示的情形是由於在執行上面的代碼時,處理器優先選擇了低優先級的進程。當這種情形發生的時候高優先級的進程也必須等待資源的釋放。這種情形在代碼中徹底有可能發生,而這種情形在編程世界中被稱爲優先級反轉

在與信號量機制不一樣的其餘編程概念中,上述情形發生後低優先級的線程會暫時繼承全部等待進程中的優先級最高進程的優先級,這被稱爲優先級繼承

飢餓線程

如今設想一個更糟糕的狀況,在當前最高和最低優先級線程中間還存在大量的默認優先級線程。在上面的優先級反轉的情形下,高優先級線程排在低優先級線程以後,可是與此同時大量的默認優先級又有可能排在低優先級線程以前(畢竟優先級高)。

這種可能的情況出現後,就會致使高優先級線程長時間處於飢餓的等待狀態。

解決方案

在我看來,信號機制應該在全部資源競爭線程的優先級相同的情形下使用。若是不知足該條件的話,我建議你看看 RegionsMonitors

死鎖

下面的示例中將有兩個線程都須要獨佔資源 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

若是幸運的話:

1*_ASgiqbV_o9caE7M7hNBpQ

簡單來講,高優先級的線程獲得了兩個資源的使用權並在執行完成後低優先級的線程繼續執行。

可是,若是運氣很差的話:

1*cVvGM-1NRH7kouSRu2mSRQ

兩個線程都沒法完成任務,他們都在等待對方釋放其手中的資源使用權。這就是計算機中死鎖概念。

解決方法

實話說,死鎖在真實世界中是很難處理的一個問題。全部,咱們在一開始的時候就應該儘可能避免這種狀況的發生。例如上例中咱們能夠將兩個資源捆綁在一塊兒作爲一個信號量,雖然在效率上可能存在必定的犧牲。

另外,在一些系統中當發生死鎖時,系統會將其中某個線程幹掉來打破這種狀態。

或者你可使用 Ostrich algorithm

相關文章
相關標籤/搜索