iOS多執行緒程式設計總結(下)

bestswifter發表於2018-01-03

#主要內容

到目前為止,我們已經瞭解了GCD和NSOperation在多執行緒程式設計中的使用。NSOperation是對GCD更高層次的封裝,提供了任務的取消、暫停、恢復功能。但GCD因為更加接近底層,所以也有自己的優勢。本章將會由淺入深,討論以下幾個部分:

  • dispatch_suspenddispatch_resume
  • dispathc_once
  • dispatch_barrier_async
  • dispatch_semaphore

#dispatch_suspend和dispatch_resume

我們知道NSOperationQueue有暫停(suspend)和恢復(resume)。其實GCD中的佇列也有類似的功能。用法也非常簡單:

dispatch_suspend(queue) //暫停某個佇列
dispatch_resume(queue)  //恢復某個佇列
複製程式碼

這些函式不會影響到佇列中已經執行的任務,佇列暫停後,已經新增到佇列中但還沒有執行的任務不會執行,直到佇列被恢復。

#dispathc_once

首先我們來看一下最簡單的* dispathc_once函式,這在單例模式中被廣泛使用。

  • dispathc_once函式可以確保某個block在應用程式執行的過程中只被處理一次,而且它是執行緒安全的。所以單例模式可以很簡單的實現,以OC中Manager類為例
+ (Manager *)sharedInstance {
static Manager *sharedManagerInstance = nil;
static dispatch_once_t once;

dispatch_once($once, ^{
sharedManagerInstance = [[Manager alloc] init];
});

return sharedManagerInstance;
}
複製程式碼

這段程式碼中我們建立一個值為nil的sharedManagerInstance靜態物件,然後把它的初始化程式碼放到dispatch_once中完成。

這樣,只有第一次呼叫sharedInstance方法時才會進行物件的初始化,以後每次只是返回sharedManagerInstance而已。

#dispatch_barrier_async

我們知道資料在寫入時,不能在其他執行緒讀取或寫入。但是多個執行緒同時讀取資料是沒有問題的。所以我們可以把讀取任務放入並行佇列,把寫入任務放入序列佇列,並且保證寫入任務執行過程中沒有讀取任務可以執行。

這樣的需求比較常見,GCD提供了一個非常簡單的解決辦法——dispatch_barrier_async

假設我們有四個讀取任務,在第二三個任務之間有一個寫入任務,程式碼大概是這樣:

let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT)

dispatch_async(queue, block1_for_reading)
dispatch_async(queue, block2_for_reading)

/*
這裡插入寫入任務,比如:
dispatch_async(queue, block_for_writing)
*/

dispatch_async(queue, block3_for_reading)
dispatch_async(queue, block4_for_reading)

複製程式碼

如果程式碼這樣寫,由於這幾個block是併發執行,就有可能在前兩個block中讀取到已經修改了的資料。如果是有多寫入任務,那問題更嚴重,可能會有資料競爭。

如果使用dispatch_barrier_async函式,程式碼就可以這麼寫:

dispatch_async(queue, block1_for_reading)
dispatch_async(queue, block2_for_reading)

dispatch_barrier_async(queue, block_for_writing)

dispatch_async(queue, block3_for_reading)
dispatch_async(queue, block4_for_reading)
複製程式碼

dispatch_barrier_async會把並行佇列的執行週期分為這三個過程:

  1. 首先等目前追加到並行佇列中所有任務都執行完成
  2. 開始執行dispatch_barrier_async中的任務,這時候即使向並行佇列提交任務,也不會執行
  3. dispatch_barrier_async中的任務執行完成後,並行佇列恢復正常。

總的來說,dispatch_barrier_async起到了“承上啟下”的作用。它保證此前的任務都先於自己執行,此後的任務也遲於自己執行。正如barrier的含義一樣,它起到了一個柵欄、或是分水嶺的作用。

這樣一來,使用並行佇列和dispatc_barrier_async方法,就可以高效的進行資料和檔案讀寫了。

dispatch_semaphore

首先介紹一下訊號量(semaphore)的概念。訊號量是持有計數的訊號,不過這麼解釋等於沒解釋。我們舉個生活中的例子來看看。

假設有一個房子,它對應程式的概念,房子裡的人就對應著執行緒。一個程式可以包括多個執行緒。這個房子(程式)有很多資源,比如花園、客廳等,是所有人(執行緒)共享的。

但是有些地方,比如臥室,最多隻有兩個人能進去睡覺。怎麼辦呢,在臥室門口掛上兩把鑰匙。進去的人(執行緒)拿著鑰匙進去,沒有鑰匙就不能進去,出來的時候把鑰匙放回門口。

這時候,門口的鑰匙數量就稱為訊號量(Semaphore)。很明顯,訊號量為0時需要等待,訊號量不為零時,減去1而且不等待。

在GCD中,建立訊號量的語法如下:

var semaphore = dispatch_semaphore_create(2)
複製程式碼

這句程式碼通過dispatch_semaphore_create方法建立一個訊號量並設定初始值為2。然後就可以呼叫dispatch_semaphore_wait方法了。

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
複製程式碼

dispatch_semaphore_wait方法表示一直等待直到訊號量的值大於等於一,當這個方法執行後,會把第一個訊號量引數的值減1。

第二個引數是一個dispatch_time_t型別的時間,它表示這個方法最大的等待時間。這在第一章中已經講過,比如 DISPATCH_TIME_FOREVER表示永久等待。

返回值也和dispatch_group_wait方法一樣,返回0表示在規定的等待時間內第一個引數訊號量的值已經大於等於1,否則表示已超過規定等待時間,但訊號量的值還是0。

dispatch_semaphore_wait方法返回0,因為此時的訊號量的值大於等於一,任務獲得了可以執行的許可權。這時候我們就可以安全的執行需要進行排他控制的任務了。

任務結束時還需要呼叫 dispatch_semaphore_signal()方法,將訊號量的值加1。這類似於之前所說的,從臥室出來要把鎖放回門上,否則後來的人就無法進入了。

我們來看一個完整的例子:

var semaphore = dispatch_semaphore_create(1)
let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT)
var array: [Int] = []

for i in 1...100000 {
dispatch_async(queue, { () -> Void in
/*
某個執行緒執行到這裡,如果訊號量值為1,那麼wait方法返回1,開始執行接下來的操作。
與此同時,因為訊號量變為0,其它執行到這裡的執行緒都必須等待
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

/*
執行了wait方法後,訊號量的值變成了0。可以進行接下來的操作。
這時候其它執行緒都得等待wait方法返回。
可以對array修改的執行緒在任意時刻都只有一個,可以安全的修改array
*/
array.append(i)

/*
排他操作執行結束,記得要呼叫signal方法,把訊號量的值加1。
這樣,如果有別的執行緒在等待wait函式返回,就由最先等待的執行緒執行。
*/
dispatch_semaphore_signal(semaphore)
})
}
複製程式碼

如果你想知道不用訊號量會出什麼問題,可以看我的另一篇文章Swift陣列append方法研究

相關文章