引言
此文是iOS中的多線程編程:重溫GCD(一)系列的第二部分。將會輔以一定的例子簡單講解一些更深層次的API使用及注意事項。
ps:為了更好的閱讀體驗,推薦戳我的個人博客:objc.in來觀看~博客也會第一時間更新在個人博客上而不是簡書上。
柵欄 | Using Barriers
使用barrier構建一個安全的讀寫操作
在上篇文章中,我們最后提到了如何利用GCD創(chuàng)建線程安全的單例。但這其實是遠遠不夠的。考慮這樣一個問題:
如果我們的單粒中有這樣的可變屬性
@property (nonatomic, copy) NSMutableArray *array;`
我們了解,在objc中,apple明確告訴我們這樣的可變集合都不是線程安全的。這意味著,如果我們的單粒在多個線程中被讀寫,很容易就發(fā)生數(shù)據(jù)混亂的問題!
dispatch barrier可以很好的解決這個問題!
根據(jù)apple官方文檔:
A dispatch barrier allows you to create a synchronization point within a concurrent dispatch queue. When it encounters a barrier, a concurrent queue delays the execution of the barrier block (or any further blocks) until all blocks submitted before the barrier finish executing. At that point, the barrier block executes by itself. Upon completion, the queue resumes its normal execution behavior.
簡單翻譯下,可見dispatch barrier允許我們在一個并發(fā)隊列中創(chuàng)建一個同步點,你也可以把它理解為一個任務(block),當隊列中的任務按序派發(fā)到這里時,并發(fā)隊列會停下等待barrier點前面的所有任務執(zhí)行完畢,接著執(zhí)行barrier block。等barrier block執(zhí)行完畢后繼續(xù)執(zhí)行后面的任務:

試想一下,如果我們在兩個線程同時在操作這個數(shù)組,如果兩個線程都在同時讀取,那不會引起問題。如果一個線程中在讀取,同時另外一個線程中在寫入,就會引起線程不安全的問題!
所以我們應該不把這個屬性暴露在外部,使得外部不能直接寫入,而是提供給外部一些方法:
- (void)add:(id)object;
- (void)remove:(id)object;
然后,我們在內(nèi)部利用barrier進行安全的寫入:
- (void)add:(id)object{
if(!object) return;
//保證寫入時,不會被隊列中的異步操作"打擾"
dispatch_barrier_async(self.concurrentQueue, ^{
[self.array addObject:object];
});
}
這樣我們就保證了在操作單例中數(shù)組的過程中,不會發(fā)生任何異步行為,也就保證了線程安全!
另外需要注意得地方是,我們在上述代碼段里使用了self.concurrentQueue為什么我們會使用self.concurrentQueue?
首先要說明的是,self.concurrentQueue是一個自定義的并發(fā)隊列,它的創(chuàng)建方式:
self.concurrentQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue",
DISPATCH_QUEUE_CONCURRENT);
我們會將單粒中的讀操作也放在這個隊列里:
- (NSArray *) array
{
__block NSArray *array;
dispatch_sync(self.concurrentQueue, ^{
array = [NSArray arrayWithArray:_photosArray];
});
return array;
}
這樣,才能保證在寫操作時候攔住前面的讀操作,因為它們都在用一個隊列中。當然,這個地方必須使用dispathc_sync函數(shù),如果異步調用意味著函數(shù)不會立即返回。那么在外部使用的得到的數(shù)組時就可能會出問題,比如:
- (NSMutableArray *)array{
__block NSMutableArray *tmpArray = nil;
dispatch_async(self.concurrentQueue, ^{
tmpArray = [_array copy];//注意這個地方可能在返回之前還沒有調用!
});
return tmpArray;//tmpArray可能為nil!
}
barrier的API
barrier是個很有用的特性,在apple的官方文檔中,有列出了下列函數(shù)供我們使用:
void dispatch_barrier_async( dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_async_f( dispatch_queue_t queue, void* context,dispatch_function_t work);
void dispatch_barrier_sync( dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_sync_f( dispatch_queue_t queue, void* context, dispatch_function_t work);
其實還是很有規(guī)律的??:async與sync相對表示同步or異步、async_f與sync相對表示執(zhí)行的是block或者dispatch_function_t對象。
由于篇幅有限,在這里就不在解釋了。只給出結論:
無論是
dispatch_barrier_syncordispatch_barrier_async函數(shù),最終都會調用到dispatch_barrier_sync_fordispatch_barrier_async_f。dispatch_barrier_sync_f函數(shù)中的void* context參數(shù)其實就是我們傳給dispatch_barrier_sync函數(shù)中的block對象。而dispatch_function_t work參數(shù)是block對象里的函數(shù)指針(懂的人自然懂??)。
barrier使用建議
要理解barrier攔住的是隊列,也就是說,barrier針對的隊列。所以不難給出以下建議:
- 不要在自定義串行隊列中使用:一個很壞的選擇,障礙不會有任何幫助,因為不管怎樣,一個串行隊列一次都只執(zhí)行一個操作。
- 不要在全局并發(fā)隊列中使用:要小心,這可能不是最好的主意,因為其它系統(tǒng)可能在使用隊列而且你不能壟斷它們只為你自己的目的。
- 最好在自定義并發(fā)隊列中使用:這對于原子或臨界區(qū)代碼來說是極佳的選擇。任何你在設置或實例化的需要線程安全的事物都是使用障礙的最佳候選。
barrier部分小結
如果想要了解更多關于barrier的實現(xiàn)細節(jié),可以自己下載GCD源碼閱讀:libdispatch。
調度組 | Dispatch Groups
使用Dispatch Groups通知所有任務已經(jīng)完成
與上文一樣,我們會以一個例子開始來介紹Dispatch Groups的部分:
假設你要從網(wǎng)上下載許多張圖片,完成之后把它們組合在一起構成新的圖像。也就是說你必須把所有圖片下載完成后再統(tǒng)一顯示:
- (void)viewDidLoad{
[super viewDidLoad];
void (^block1)() = ^{
//download img_1 from network...
};
void (^block2)() = ^{
//download img_2 from network...
};
void (^block3)() = ^{
//download img_2 from network...
};
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), block1);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), block2);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), block3);
//拼接圖片
[self finishImg];
}
這樣寫顯然是有問題的!block1,block2,block3的運行由于是在后臺,我們無法確保拼接圖片時所有圖片已經(jīng)下載完成。對于這種情況,Dispatch Groups就是個不錯的選擇:
方法一:使用dispatch_group_notify
- (void)viewDidLoad{
[super viewDidLoad];
void (^block1)() = ^{
//download img1 from network...
NSLog(@"block1 finish");
};
void (^block2)() = ^{
//download img2 from network...
NSLog(@"block2 finish");
};
void (^block3)() = ^{
//download img3 from network...
NSLog(@"block3 finish");
};
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_t downloadGroup = dispatch_group_create();
dispatch_group_async(downloadGroup, queue, block1);
dispatch_group_async(downloadGroup, queue, block2);
dispatch_group_async(downloadGroup, queue, block3);
dispatch_group_notify(downloadGroup, queue, ^{
[self finishImg];
});
}
- (void)finishImg{
NSLog(@"finishi..");
}
這樣,finishImg方法就會在三個block全部執(zhí)行完畢后才被調用:
2016-08-27 15:12:37.885 總結測試[91285:7916599] block3 finish
2016-08-27 15:12:37.885 總結測試[91285:7916593] block2 finish
2016-08-27 15:12:37.885 總結測試[91285:7916604] block1 finish
2016-08-27 15:12:37.886 總結測試[91285:7916604] finishi..
dispatch_group_notify函數(shù)非常靈活,它允許你在group內(nèi)的任務全部完成后傳遞一個block作為回調。在上述的方法里,我們將圖片拼接方法作為回調。
除了dispatch_group_notify函數(shù),還有個dispatch_group_t字眼你可能會感到陌生,在蘋果的官方文檔中:
A group of block objects submitted to a queue for asynchronous invocation.
Declaration
typedef struct dispatch_group_s *dispatch_group_t;
Discussion
A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.
Note that the blocks in a group may be run on different queues, and each individual block can add more blocks to the group.
The dispatch group keeps track of how many blocks are outstanding, and GCD retains the group until all its associated blocks complete execution.
可見group是異步block的集合。而dispatch group是一種監(jiān)聽這種集合的機制。蘋果允許我們在同步或者異步的監(jiān)聽集合里的block完成的情況,而且需要注意到是,group內(nèi)的block可能是在任何隊列里的。就像我們上面的代碼中寫的那樣:
dispatch_group_async(downloadGroup, queue, block1);
dispatch_group_async(downloadGroup, queue, block2);
dispatch_group_async(downloadGroup, queue, block3);
我們可以提交group內(nèi)的任務到任何queue內(nèi)。
順帶一提的是,gcd會引用group對象直到任務都完成。
方法二:使用dispatch_group_wait
dispatch_group_wait函數(shù)就像它的名字所敘述的那樣:等待
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
dispatch_group_wait函數(shù)會等待直到指定的group內(nèi)的任務全部完成或者超時,從上方的申明我們可以看出,dispatch_group_wait與dispatch_group_wait函數(shù)一樣都需要一個group參數(shù),但是多了一個timeout替代block回調。同時,它還會有一個long型返回值。這個返回時標志了當前任務的執(zhí)行情況或者是否超時:
dispatch_group_wait會一直等待,直到任務全部完成或者超時。如果在所有任務完成前超時了,該函數(shù)會返回一個非零值。你可以對此返回值做條件判斷以確定是否超出等待周期;當然,你可以在這里用DISPATCH_TIME_FOREVER讓它永遠等待。它的意思,勿庸置疑就是,永-遠-等-待!于是我們只需要判斷返回值是否為0就可以知道當前任務是否完成了。
注意,一定要理解好等待。這意味著dispatch_group_wait會阻塞當前線程!另外,如果你不使用DISPATCH_TIME_FOREVER參數(shù)而是用DISPATCH_TIME_NOW,該函數(shù)會立即返回一個值給你,含義與DISPATCH_TIME_FOREVER一樣。當然了,這樣的話就不會阻塞當前線程了。但是需要像主線程里的NSRunLoop那樣不停循環(huán)檢測當前任務是否全部完成。
了解了dispatch_group_wait函數(shù)的含義,我們不難將第一種方法里的代碼改寫為:
- (void)viewDidLoad{
[super viewDidLoad];
self.view.backgroundColor = [UIColor grayColor];
void (^block1)() = ^{
//download img1 from network...
for (int i = 0 ; i < 500; i ++) {
NSLog(@"block1 finish %d",i);
}
};
void (^block2)() = ^{
//download img2 from network...
for (int i = 0 ; i < 500; i ++) {
NSLog(@"block2 finish %d",i);
}
};
void (^block3)() = ^{
//download img3 from network...
for (int i = 0 ; i < 500; i ++) {
NSLog(@"block3 finish %d",i);
}
};
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_t downloadGroup = dispatch_group_create();
dispatch_group_async(downloadGroup, queue, block1);
dispatch_group_async(downloadGroup, queue, block2);
dispatch_group_async(downloadGroup, queue, block3);
dispatch_async(queue, ^{
long result = dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER);
if (result == 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self finishImg];
});
}
});
}
需要格外注意的是:由于dispatch_group_wait函數(shù)會阻塞當前線程,所以我們使用了dispatch_async函數(shù)以便能更快的使viewDidLoad方法結束以給予用戶更好的體驗。
并行運行的for循環(huán):dispatch_apply
dispatch_apply函數(shù)會將block按指定的次數(shù)提交到指定的隊列里去,之后等待所有任務完成后返回:
- (void)viewDidLoad{
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_apply(10, queue, ^(size_t x) {
NSLog(@"%zu current thead == %@", x , [NSThread currentThread]);
});
NSLog(@"finishi!");
}
執(zhí)行結果:

有趣的信息很多,我們可以看到每個任務完成的順序并不一定,而且所在的線程也不一定。(number不為1的話都是子線程)在所有任務都完成后,才會打印finish。
這是因為dispatch_apply函數(shù)會阻塞住當前線程,這和dispatch_sync是一樣的。所以推薦在dispath_async函數(shù)中異步地執(zhí)行dispatch_apply函數(shù)。當然了,關于隊列的選擇上肯定也要是并發(fā)隊列,否則沒有任何意義:
- (void)viewDidLoad{
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
dispatch_apply(10, queue, ^(size_t x) {
NSLog(@"%zu current thead == %@", x , [NSThread currentThread]);
});
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"finish %@",[NSThread currentThread]);
});
});
}
另外,在這篇文章中作者也提到了:使用dispatch_apply函數(shù)開辟線程來執(zhí)行任務可能要比for循環(huán)代價大得多,所以使用前要三思。
信號量 | Semaphore
有趣的哲學家問題
在了解信號量之前,可以先看看這個有趣的問題:哲學家就餐問題。
以下摘自維基百科:
信號量(英語:Semaphore)又稱為信號量、旗語,它以一個整數(shù)變量,提供信號,以確保在并行計算環(huán)境中,不同進程在訪問共享資源時,不會發(fā)生沖突。是一種不需要使用忙碌等待(busy waiting)的一種方法。
信號量的概念是由荷蘭計算機科學家艾茲格·迪杰斯特拉(Edsger W. Dijkstra)發(fā)明的,廣泛的應用于不同的操作系統(tǒng)中。在系統(tǒng)中,給予每一個進程一個信號量,代表每個進程目前的狀態(tài),未得到控制權的進程會在特定地方被強迫停下來,等待可以繼續(xù)進行的信號到來。如果信號量是一個任意的整數(shù),通常被稱為計數(shù)信號量(Counting semaphore),或一般信號量(general semaphore);如果信號量只有二進制的0或1,稱為二進制信號量(binary semaphore)。在linux系中,二進制信號量(binary semaphore)又稱Mutex。
一個錯誤的例子
回到我們的代碼中,在上一篇iOS中的多線程編程:重溫GCD(一)中我曾介紹過:NSMutableArray不是線程安全的,所以以下的寫法會有很大的問題:
- (void)viewDidLoad{
[super viewDidLoad];
NSMutableArray *array = [NSMutableArray array];
_array = array;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0 ; i < 100; i ++) {
dispatch_async(queue, ^{
[_array addObject:[NSString stringWithFormat:@"%d",i]];
});
}
}
你可以試試運行上述代碼,很容易引發(fā)crash。此時就可以使用 Dispatch Semaphore 來避免這個問題。
利用 Dispatch Semaphore 來解決
摘自:《objc高級編程:iOS 與 OS X 多線程與內(nèi)存管理》:
Dispatch Semaphore 是持有計數(shù)的信號,該計數(shù)是多線程編程中的計數(shù)類型信號。所謂信號,是類似于過馬路時常用的手旗,可以通過時舉起手旗,不可通過時放下手旗。而在 Dispatch Semaphore 中,使用計數(shù)來實現(xiàn)此功能。計數(shù)為0時等待,計數(shù)為1或者大于1時,減去1而不等待。
創(chuàng)建 dispatch_semaphore_t
上文中我們提到 Dispatch Semaphore 就像是手旗,而dispatch_semaphore_t變量就是那個手旗對象。我們可以這樣創(chuàng)建它:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
其中,參數(shù)是long類型:
value
The starting value for the semaphore. Passing a value less than zero causes NULL to be returned.
有了"手旗"??,我們就可以用起來啦:
開始使用信號量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait函數(shù)第一個參數(shù)天我們創(chuàng)建出來的手旗對象,第二個參數(shù)與上文提到的dispatch_group_wait函數(shù)中類似,意味著等待到永遠。這正是我們要想的!
于是,我們就可以像下面這樣解決我們遇到的問題:
- (void)viewDidLoad{
[super viewDidLoad];
NSMutableArray *array = [NSMutableArray array];
_array = array;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0 ; i < 100; i ++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//永遠等待信號量 >= 1 只有在信號量>=1時才能進行下一步操作 !
[_array addObject:[NSString stringWithFormat:@"%d",i]];
NSLog(@"finish in %@ currentThread",[NSThread currentThread]);
dispatch_semaphore_signal(semaphore);//處理完后 將信號量+1 避免出現(xiàn)問題
});
}
}
當然,Dispatch Semaphore 其實更多時候用來處理更加需要“細粒”化得情形,比如上面這種并發(fā)處理線程不安全的數(shù)組時,用dispatch_barrier一樣可以做到,但是無法像 Dispatch Semaphore 這么細?;1热缒阋l(fā)處理某些事情,但是只需要在特定的情形下才需要線程安全,信號量就是個更好的選擇,而不是用barrier
The End
耗時近一周終于寫完了這兩篇文章,收獲非常大。在后續(xù)的篇章中我簡單介紹下關于GCD的實現(xiàn),歡迎大家圍觀??。