GCD 併發佇列

發表於2016-10-14

本篇文章由我們團隊的郭傑童鞋翻譯完成。這是關於GCD系列的第三篇文章,原文是GCD Concurrent Queues


如果說序列佇列是互斥量更好的替代品的話,那麼併發佇列就是執行緒的一個更好的替代品。

併發佇列可以讓你入隊的block且併發執行,而不需要等待前面入隊的block完成執行。

多次執行以下程式:

dispatch_async()告訴GCD來入隊block塊,而不是等到block塊移動之前完成。這就使我們能夠快速的將5個block置於我們剛剛建立的併發佇列中。

當第一個block塊入隊時,佇列是空的,因此如果當前佇列是序列,它會按照同樣的方式來執行。但是,當第二個block被入隊時,即使第一個block還沒有執行完成,第二個也依然會執行。當然了,這種方式也同樣適用於第三個、第四和第五個block,它們都會在同一時間開始執行。

佇列上的每一個block在建立時會捕獲index索引值, 並會列印10次記錄。這個程式的輸出符合您的預期嗎?為什麼每次執行程式的輸出結果都是不一樣的呢?

如果我們使用序列佇列的話,程式的輸出會有什麼不同呢?嘗試將DISPATCH_QUEUE_CONCURRENT修改為DISPATCH_QUEUE_SERIAL,然後再次執行程式,試試看。

用佇列,不用執行緒

你可能已經錯過了執行緒,但是上面的程式在不使用pthread_create()NSThread的情況下,毫不費力地建立並執行五個執行緒。由於在併發佇列中每一個block都必須是同時執行的,GCD會自動建立(或徵用)一個執行緒來執行它們中的每一個。每個block一旦完成,該執行緒會被摧毀或者返回到一個執行緒池中。使用GCD,你可以專注於佇列,並讓有關執行緒庫去考慮執行緒問題。

雖然你不用手動管理執行緒,但這並不意味著你可以忽視執行緒的限制。如果入隊的併發block比可用的執行緒更多,你的程式可能會出問題。

障礙(Barriers)

在這個點上的一個很自然的問題是:如果併發佇列允許所有的block執行,那麼為什麼它們被稱為”佇列“呢?它不是更像一個可以加入併發執行block的堆嗎?

當你考慮障礙時,併發佇列的行為看起來就像佇列了。使用dispatch_barrier_sync()或者dispatch_barrier_async()入隊的block會帶來一些有意思的事情:這個block塊將會被入隊,但是會等到所有的之前入隊的block執行完成後才開始執行。除此之外,在barrier block後面入隊的所有的block,會等到到barrier block本身已經執行完成之後才繼續執行。barrier block通常被看作是一系列的併發操作集合中的”choke points”(咽喉要道)。

為了展示障礙block,看看下面的程式:

執行這個程式。可以注意到barrier之前的那些block,只有索引為0到4的block是被允許執行到完成,而在這個barrier之後,僅僅序號為5到9的block會被執行。然而,在barrier兩邊,每組5個block塊被允許在同一個時間執行。

讀寫鎖

在我上一篇部落格中,我講了如何使用序列佇列去保護一組狀態變數。使用這個技術,僅有一個執行緒可以在一個時間內訪問一個變數,從而保證了原子行為。

但是說實話,我們沒有必要在讀取資料時去保護這些資料:我們僅僅需要在非同步修改時去保護它們。允許多個執行緒讀取資料而不改變資料從某些角度(如效能)來說是非常好的。

我們需要的是一個讀寫鎖,即寫入的時候序列化訪問操作,但是允許多個讀操作併發。

我們可以使用非同步佇列和barrier輕鬆實現一個讀寫鎖。如下所示:

你現在可以忽略呼叫dispatch_after(),它只是簡單的告訴GCD在一段時間後入隊一個block。

在這個例子當中,barrier block是保證了修改操作的原子性。因為barrier block總是不間斷的執行,你將不會看到有“Luke like Alice!”列印出來。

恭喜你!你已經瞭解了關於併發佇列的所有內容,以及它們是怎樣被用來代替執行緒的並建立有效的讀寫鎖。在下一篇文章中,我們將解析全域性併發佇列和目標佇列。下次見!

相關文章