
這是GCD介紹的第四篇文章。
跟我一起"閑逛"一會,看一下GCD的一個實用的功能:目標隊列(target queues)。
開啟旅程之前,我們先學習一種特殊的隊列:全局并發(fā)隊列(the global concurrent queues)。
全局并發(fā)隊列(Global concurrent queues)
GCD給我們的程序提供了4種全局并發(fā)隊列。這些隊列非常特殊,因為它們是由庫自動創(chuàng)建的,永遠不會被阻塞的,并且它們處理障礙block和一般的block一樣。因為它們是并發(fā)的,所以所有入隊的block會一起并行執(zhí)行。
這四種全局并發(fā)隊列有不同的優(yōu)先級:
DISPATCH_QUEUE_PRIORITY_HIGHDISPATCH_QUEUE_PRIORITY_DEFAULTDISPATCH_QUEUE_PRIORITY_LOWDISPATCH_QUEUE_PRIORITY_BACKGROUND
高優(yōu)先級隊列中的block會搶占低優(yōu)先級隊列中block的資源。
這些全局并發(fā)隊列在GCD中扮演了線程優(yōu)先級的角色。像線程一樣,高優(yōu)先級隊列中的block有可能搶占CPU所有的資源,使得低優(yōu)先級隊列中的block無法執(zhí)行。
你可以用這種方法獲得一個全局并發(fā)隊列:
dispatch_queue_t defaultPriorityGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
目標隊列
那么我們怎么來使用這些全局并發(fā)隊列呢?令人驚訝的是,你已經(jīng)在使用它們了!每一個你創(chuàng)建的隊列都必須有一個目標隊列。默認情況下, 是優(yōu)先級為DISPATCH_QUEUE_PRIORITY_DEFAULT的全局并發(fā)隊列。
擁有一個目標隊列對一個普通的隊列來說有什么意義呢?答案可能有點令人意外:隊列里每一個準備好要執(zhí)行的block,將會被重新加入到這個隊列的目標隊列里去執(zhí)行。
但是等一下,我們不是一直假設(shè)block就在其所在的隊列里執(zhí)行嗎?難道這都是騙人的嗎?
也不見得。因為所有新建的的隊列會把默認優(yōu)先級的全局并發(fā)隊列當做其目標隊列,所以不管哪一個隊列上,任何一個準備好將要執(zhí)行的block基本上都會立即執(zhí)行。除非你改變隊列的目標隊列,否則這些block看起來就是在你的隊列中執(zhí)行的。
你的隊列繼承了其目標隊列的優(yōu)先級。將你的隊列的目標隊列改為更高或更低優(yōu)先級的全局并發(fā)隊列,能有效的改變你的隊列的優(yōu)先級。
<p>
只有全局并發(fā)隊列和主隊列才能執(zhí)行block。其他所有的隊列最終都必須設(shè)置其中一個為它的目標隊列。
目標隊列實踐
讓我們來看個例子。
幾代人以前,我們很多人的祖父母家的電話都被連接到了一個共用線路。這是在一個社區(qū)的所有電話都連接到一個單回路的布置,任何一個人拿起電話就能聽見其他正在打電話的人在說什么。
假設(shè)我們有2組人,住在2座房子里,house1Folks和house2Folks,他們連接到了一個共用線路上。1號房子的人喜歡給2號房子的人打電話,問題是,他們打電話前沒人會去檢查當前是否有其他人在打電話。讓我們看一下:
// Party line!
#import <Foundation/Foundation.h>
void makeCall(dispatch_queue_t queue, NSString *caller, NSArray *callees) {
// Randomly call someone
NSInteger targetIndex = arc4random() % callees.count;
NSString *callee = callees[targetIndex];
NSLog(@"%@ is calling %@...", caller, callee);
sleep(1);
NSLog(@"...%@ is done calling %@.", caller, callee);
// Wait some random time and call again
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (arc4random() % 1000) * NSEC_PER_MSEC), queue, ^{
makeCall(queue, caller, callees);
});
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *house1Folks = @[@"Joe", @"Jack", @"Jill"];
NSArray *house2Folks = @[@"Irma", @"Irene", @"Ian"];
dispatch_queue_t house1Queue = dispatch_queue_create("house 1", DISPATCH_QUEUE_CONCURRENT);
for (NSString *caller in house1Folks) {
dispatch_async(house1Queue, ^{
makeCall(house1Queue, caller, house2Folks);
});
}
}
dispatch_main();
return 0;
}
運行這段程序看看會發(fā)生什么:
Jack is calling Ian...
...Jack is done calling Ian.
Jill is calling Ian...
Joe is calling Ian...
...Jill is done calling Ian.
...Joe is done calling Ian.
Jack is calling Irene...
...Jack is done calling Irene.
Jill is calling Irma...
Joe is calling Ian...
真是太亂了!沒有等上一次通話結(jié)束,新的電話就被接通了。讓我們看看能不能解決這個問題。創(chuàng)建一個串行隊列并把它作為house1Queue的目標隊列。
// ...
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *house1Folks = @[@"Joe", @"Jack", @"Jill"];
NSArray *house2Folks = @[@"Irma", @"Irene", @"Ian"];
dispatch_queue_t house1Queue = dispatch_queue_create("house 1", DISPATCH_QUEUE_CONCURRENT);
// Set the target queue
dispatch_queue_t partyLine = dispatch_queue_create("party line", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(house1Queue, partyLine);
for (NSString *caller in house1Folks) {
dispatch_async(house1Queue, ^{
makeCall(house1Queue, caller, house2Folks);
});
}
}
dispatch_main();
return 0;
}
結(jié)果如下:
Joe is calling Ian...
...Joe is done calling Ian.
Jack is calling Irma...
...Jack is done calling Irma.
Jill is calling Irma...
...Jill is done calling Irma.
Joe is calling Irma...
...Joe is done calling Irma.
Jack is calling Irene...
...Jack is done calling Irene.
好多了!
可能不會被馬上發(fā)現(xiàn),并發(fā)隊列里的block以先進先出(FIFO)的順序被執(zhí)行,也就是說先入隊的block將會被先執(zhí)行。但是一個并發(fā)隊列里的block并不會等待前一個block執(zhí)行完畢才會開始執(zhí)行,之后的block應(yīng)該一起開始執(zhí)行。
我們知道一個隊列里的block實際上并不是在這個隊列上運行的,而是把準備好要執(zhí)行的block重新入隊到其目標隊列里去執(zhí)行。當你把一個并發(fā)隊列的目標隊列設(shè)置為一個串行隊列時,這個并發(fā)隊列就會把其上的block以先進先出的順序入隊到那個串行隊列中,也就是其目標隊列。又因為串行隊列里的block必須等待其前一個block執(zhí)行完畢才會開始執(zhí)行,所以那些最開始入隊到并發(fā)隊列的block將被迫以串行的方式執(zhí)行??偟膩碚f,串行目標隊列能夠串行化一個并發(fā)隊列。
house1Queue隊列的目標隊列是partyLine,partyLine隊列的目標隊列是默認優(yōu)先級的全局并發(fā)隊列,所以,house1Queue上的block會被重新入隊到partyLine隊列,然后再被入隊到全局并發(fā)隊列并執(zhí)行。
<p>
設(shè)置一堆目標隊列有可能產(chǎn)生一個循環(huán),使你的目標隊列最終指向最開始的那個隊列。這樣做會產(chǎn)生不可預知的后果,所以別這么做。
多個隊列設(shè)置同一個目標隊列
多個隊列可以設(shè)置同一個隊列為其目標隊列。2號房子的人們也希望打電話給1號房子中的人,讓我們?yōu)樗麄儎?chuàng)建一個隊列,并且設(shè)置partyLine隊列為其目標隊列。
// Party line!
#import <Foundation/Foundation.h>
void makeCall(dispatch_queue_t queue, NSString *caller, NSArray *callees) {
// Randomly call someone
NSInteger targetIndex = arc4random() % callees.count;
NSString *callee = callees[targetIndex];
NSLog(@"%@ is calling %@...", caller, callee);
sleep(1);
NSLog(@"...%@ is done calling %@.", caller, callee);
// Wait some random time and call again
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (arc4random() % 1000) * NSEC_PER_MSEC), queue, ^{
makeCall(queue, caller, callees);
});
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *house1Folks = @[@"Joe", @"Jack", @"Jill"];
NSArray *house2Folks = @[@"Irma", @"Irene", @"Ian"];
dispatch_queue_t house1Queue = dispatch_queue_create("house 1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t house2Queue = dispatch_queue_create("house 2", DISPATCH_QUEUE_CONCURRENT);
// Set the target queue for BOTH house queues
dispatch_queue_t partyLine = dispatch_queue_create("party line", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(house1Queue, partyLine);
dispatch_set_target_queue(house2Queue, partyLine);
for (NSString *caller in house1Folks) {
dispatch_async(house1Queue, ^{
makeCall(house1Queue, caller, house2Folks);
});
}
for (NSString *caller in house2Folks) {
dispatch_async(house2Queue, ^{
makeCall(house2Queue, caller, house1Folks);
});
}
}
dispatch_main();
return 0;
}
運行這段程序,發(fā)現(xiàn)了什么?
由于2個并發(fā)隊列的目標隊列設(shè)置為了同一個串行隊列,所以2個并發(fā)隊列中的block將會被一個接一個的執(zhí)行。一個串行隊列串行化了以其為目標隊列的2個并發(fā)隊列。
將其中一個或全部隊列的目標隊列移除,看看會發(fā)生什么。結(jié)果在你意料之中嗎?
目標隊列的實際應(yīng)用
目標隊列可以應(yīng)用在一些優(yōu)雅的設(shè)計中。在上面的例子中,我們用了一個或多個并發(fā)隊列并且串行化了它們的執(zhí)行操作。設(shè)定一個串行隊列為目標隊列也就表明了,不管有多少不同的線程在競爭資源,同一時間只做一件事。這個“一件事”可能是一個數(shù)據(jù)庫請求,訪問物理磁盤驅(qū)動,或者操作一些硬件資源。
如果有一些block必須被并發(fā)執(zhí)行程序才能繼續(xù)運行,那么給一個并發(fā)隊列設(shè)置一個串行目標隊列,可能會造成死鎖。要謹慎使用這種模式。
當你想要協(xié)調(diào)不同來源的異步時間時,串行目標隊列是很重要的,比如計時器,網(wǎng)絡(luò)時間,文件系統(tǒng)等等。當你需要協(xié)調(diào)一些來自不同框架的對象的事件時,或者你不能更改一個類的源代碼時,串行目標隊列也會相當有用。在以后的文章中我會談一談計時器和其他一些事件源。
正如我的同事Mike E.所說的:把一個串行隊列設(shè)置為一個并發(fā)隊列的目標隊列并沒有實際的應(yīng)用的意義。我傾向于他的觀點:我很難找到一個例子,設(shè)置并發(fā)隊列的目標隊列為串行隊列要優(yōu)于直接
dispatch_async到一個串行隊列上。
并發(fā)目標隊列給你另一種魔力:你可以讓block以它們原來的方式繼續(xù)執(zhí)行,除非你入隊了一個障礙block(barrier block)。如果你這樣做了,將會使得所有入隊的block暫停執(zhí)行,直到當前正在執(zhí)行的blcok和障礙block執(zhí)行完畢再恢復執(zhí)行。這就像多條操作流的一個總開關(guān),你可以在恢復執(zhí)行前做一些其他的工作。
最后
到這里,目標隊列也說的差不多了。如果你剛開始接觸GCD,我知道這些內(nèi)容對你來說短時間內(nèi)可能有點難消化。實際上,你完全可以繼續(xù)快樂地走在原來的學習路線上,不用停下來去了解目標隊列。但是如果有一天,你被一個問題困擾,你突然發(fā)現(xiàn)這個問題可以用目標隊列的方式優(yōu)雅的解決,那么我們的這次"閑逛"就值得了。
我希望你能享受這次"閑逛"。下次再見,我將會談一談如何設(shè)計類,使其能夠配合GCD更好的工作。