本篇文章由我們團隊的郭傑童鞋翻譯完成。這是關於GCD系列的第三篇文章,原文是GCD Concurrent Queues。
如果說序列佇列是互斥量更好的替代品的話,那麼併發佇列就是執行緒的一個更好的替代品。
併發佇列可以讓你入隊的block且併發執行,而不需要等待前面入隊的block完成執行。
多次執行以下程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#import <Foundation/Foundation.h> void print(int number) { for (int count = 0; count < 10; ++count) { NSLog(@"%d", number); } } int main(int argc, const char * argv[]) { dispatch_queue_t queue = dispatch_queue_create("My concurrent queue", DISPATCH_QUEUE_CONCURRENT); @autoreleasepool { for (int index = 0; index < 5; ++index) { dispatch_async(queue, ^{ print(index); }); } } dispatch_main(); return 0; } |
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,看看下面的程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#import <Foundation/Foundation.h> void print(int number) { for (int count = 0; count < 10; ++count) { NSLog(@"%d", number); } } int main(int argc, const char * argv[]) { dispatch_queue_t queue = dispatch_queue_create("My concurrent queue", DISPATCH_QUEUE_CONCURRENT); dispatch_suspend(queue); // Suspend the queue so blocks are enqueued, but not executed @autoreleasepool { // Enqueue five blocks for (int index = 0; index < 5; ++index) { dispatch_async(queue, ^{ print(index); }); } // Enqueue a barrier dispatch_barrier_async(queue, ^{ NSLog(@"--- This is a barrier ---"); }); // Enqueue five more blocks for (int index = 5; index < 10; ++index) { dispatch_async(queue, ^{ print(index); }); } } dispatch_resume(queue); // Go! dispatch_main(); return 0; } |
執行這個程式。可以注意到barrier之前的那些block,只有索引為0到4的block是被允許執行到完成,而在這個barrier之後,僅僅序號為5到9的block會被執行。然而,在barrier兩邊,每組5個block塊被允許在同一個時間執行。
讀寫鎖
在我上一篇部落格中,我講了如何使用序列佇列去保護一組狀態變數。使用這個技術,僅有一個執行緒可以在一個時間內訪問一個變數,從而保證了原子行為。
但是說實話,我們沒有必要在讀取資料時去保護這些資料:我們僅僅需要在非同步修改時去保護它們。允許多個執行緒讀取資料而不改變資料從某些角度(如效能)來說是非常好的。
我們需要的是一個讀寫鎖,即寫入的時候序列化訪問操作,但是允許多個讀操作併發。
我們可以使用非同步佇列和barrier輕鬆實現一個讀寫鎖。如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#import <Foundation/Foundation.h> dispatch_queue_t queue; NSString *he = @"Luke"; NSString *she = @"Megan"; void printAndRepeat() { NSLog(@"%@ likes %@!", he, she); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, // This block is dispatch_async'd to the concurrent queue after 1 second ^{ printAndRepeat(); }); } int main(int argc, const char * argv[]) { @autoreleasepool { queue = dispatch_queue_create("Reader-writer queue", DISPATCH_QUEUE_CONCURRENT); // Create readers for (int index = 0; index < 5; ++index) { dispatch_async(queue, ^{ printAndRepeat(); }); } // Change the variables after 5 seconds dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), // This block is enqueued onto the main queue after 5 seconds. ^{ dispatch_barrier_async(queue, ^{ he = @"Don"; she = @"Alice"; }); }); } dispatch_main(); return 0; } |
你現在可以忽略呼叫
dispatch_after()
,它只是簡單的告訴GCD在一段時間後入隊一個block。
在這個例子當中,barrier block是保證了修改操作的原子性。因為barrier block總是不間斷的執行,你將不會看到有“Luke like Alice!”列印出來。
恭喜你!你已經瞭解了關於併發佇列的所有內容,以及它們是怎樣被用來代替執行緒的並建立有效的讀寫鎖。在下一篇文章中,我們將解析全域性併發佇列和目標佇列。下次見!