本文翻譯自GCD Target Queues,個(gè)人覺(jué)得寫(xiě)得很不錯(cuò)饲帅,準(zhǔn)備翻譯一遍复凳,查了一下卻發(fā)現(xiàn)已經(jīng)有前輩翻譯過(guò)了瘤泪,不過(guò)還是自己自動(dòng)翻譯了一遍,在不改變?cè)獾那疤嵯掠耍隽艘恍┬薷亩酝荆尤胍恍┳约旱睦斫狻7g有參考這篇 GCD目標(biāo)隊(duì)列(GCD Target Queues)(侵刪)髓棋。
這是 GCD 系列文章 第四篇实檀。
我們來(lái)看看 GCD 的一個(gè)優(yōu)雅特性:目標(biāo)隊(duì)列。
在開(kāi)始之前學(xué)習(xí)一些列隊(duì)列之前仲锄,我們先來(lái)看看特殊的一種隊(duì)列:全局隊(duì)列劲妙。
全局隊(duì)列(Global concurrent queues)
GCD 提供四種可用的并發(fā)全局隊(duì)列,這些隊(duì)列比較特殊儒喊,因?yàn)樗怯上到y(tǒng)庫(kù)自動(dòng)創(chuàng)建的镣奋,永遠(yuǎn)不會(huì)被阻塞。即使遇到障礙(barrier blocks) 任務(wù)怀愧,其他任務(wù)也不會(huì)被阻塞侨颈。這些隊(duì)列都是并發(fā)的,所有入隊(duì)列的任務(wù)都會(huì)并發(fā)執(zhí)行芯义。
四種隊(duì)列對(duì)應(yīng)的優(yōu)先級(jí)分別是:
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
- DISPATCH_QUEUE_PRIORITY_BACKGROUND
高優(yōu)先級(jí)隊(duì)列里的任務(wù)比低優(yōu)先級(jí)隊(duì)列里的任務(wù)優(yōu)先執(zhí)行哈垢。
GCD 的這些全局隊(duì)列同時(shí)也扮演一個(gè)線(xiàn)程優(yōu)先級(jí)的角色。像線(xiàn)程一樣扛拨,執(zhí)行在高優(yōu)先級(jí)的隊(duì)列中的任務(wù)會(huì)搶占 CPU 資源耘分,使得低優(yōu)先級(jí)的隊(duì)列處于「饑餓」?fàn)顟B(tài),即無(wú)法得到執(zhí)行绑警。
你可以用如下方式獲取全局的并發(fā)隊(duì)列:
dispatch_queue_t defaultPriorityGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
目標(biāo)隊(duì)列(Target Queue)
這些全局隊(duì)列該如何使用呢求泰?說(shuō)出來(lái)可能會(huì)令你驚訝:你已經(jīng)在使用它們了。任何隊(duì)列一旦你創(chuàng)建都會(huì)對(duì)應(yīng)有一個(gè)目標(biāo)隊(duì)列计盒。默認(rèn)情況下渴频,所有隊(duì)列(主隊(duì)列除外)都會(huì)把 DISPATCH_QUEUE_PRIORITY_DEFAULT
優(yōu)先級(jí)的全局并發(fā)隊(duì)列作為目標(biāo)隊(duì)列。
隊(duì)列竟然還有目標(biāo)隊(duì)列北启?是不是有點(diǎn)驚訝:實(shí)際上每當(dāng)隊(duì)列中的任務(wù)準(zhǔn)備好要執(zhí)行的時(shí)候卜朗,隊(duì)列會(huì)把該任務(wù)重入(重新放入)到目標(biāo)隊(duì)列,真正的執(zhí)行在目標(biāo)隊(duì)列中咕村。
等等场钉,難道任務(wù)不是在我們所提交的隊(duì)列里面執(zhí)行的?這些全都是謊言懈涛?
不是的惹悄,因?yàn)樗凶约簞?chuàng)建的隊(duì)列(包括串行隊(duì)列)都會(huì)把默認(rèn)優(yōu)先級(jí)的全局并發(fā)隊(duì)列當(dāng)做目標(biāo)隊(duì)列,前面說(shuō)過(guò)全局并發(fā)隊(duì)列不會(huì)被阻塞肩钠,等待工作都是在提交的隊(duì)列中的泣港,一旦輪到執(zhí)行,就會(huì)被提交到目標(biāo)隊(duì)列中价匠,并立刻開(kāi)始執(zhí)行当纱。所以除非是你自定義目標(biāo)隊(duì)列,否則你完全可以抽象的認(rèn)為任務(wù)就是在你提交的隊(duì)列中開(kāi)始執(zhí)行的踩窖。
你創(chuàng)建的隊(duì)列的優(yōu)先級(jí)繼承自目標(biāo)全局隊(duì)列坡氯。把目標(biāo)全局隊(duì)列的優(yōu)先級(jí)設(shè)置的更高或者更低,會(huì)同時(shí)改變你創(chuàng)建隊(duì)列的優(yōu)先級(jí)洋腮。
只有全局隊(duì)列和主隊(duì)列會(huì)執(zhí)行任務(wù)箫柳,它們是所有其他隊(duì)列的目標(biāo)隊(duì)列。
來(lái)看一張來(lái)自 objc 的圖:
該圖說(shuō)明了自定義隊(duì)列分成兩路:一路是串行隊(duì)列進(jìn)入 GCD 的主隊(duì)列啥供,另一路是進(jìn)入 GCD 默認(rèn)優(yōu)先級(jí)的全局并發(fā)隊(duì)列悯恍。最后都在系統(tǒng)維護(hù)的線(xiàn)程池中被執(zhí)行。
玩轉(zhuǎn)目標(biāo)隊(duì)列(Party time with target queues)
先來(lái)看一個(gè)列子伙狐。
很久以前涮毫,我們的祖先使用公用線(xiàn)路打電話(huà)的。一個(gè)社區(qū)里面的電話(huà)都是在一條線(xiàn)路上通信的贷屎,任何人只要拿起電話(huà)罢防,就可以聽(tīng)到其他人打電話(huà)的的聲音。
假設(shè)我們有兩小組的人唉侄,住在兩間房子里面咒吐,通過(guò)單一的線(xiàn)路連接: house1Folks 和 house2Folks。房間一的人喜歡給房間二的人打電話(huà)属划。問(wèn)題是恬叹,他們?cè)诖螂娫?huà)前不回去檢查是否有其他人在打電話(huà)。來(lái)看例子:
// Party line!
#import <Foundation/Foundation.h>
void makeCall(dispatch_queue_t queue, NSString *caller, NSArray *callees) {
// caller 隨機(jī)打電話(huà)給 callees 里面的一個(gè)人
NSInteger targetIndex = arc4random() % callees.count;
NSString *callee = callees[targetIndex];
NSLog(@"%@ is calling %@...", caller, callee);
sleep(1);
NSLog(@"...%@ is done calling %@.", caller, callee);
// 隨機(jī)等待一段時(shí)間榴嗅,然后接著打
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);
});
}
}
return 0;
}
運(yùn)行程序來(lái)看看結(jié)果:
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...
實(shí)在太亂了妄呕!他們都沒(méi)有等前面的的人掛斷電話(huà),就自己開(kāi)始打電話(huà)了嗽测。我們來(lái)看看能不能捋一捋绪励,創(chuàng)建一個(gè)串行隊(duì)列作為 house1Queue 的目標(biāo)隊(duì)列。
// ...
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.
現(xiàn)在好多了唠粥。
這可能不太明顯疏魏,但是并發(fā)隊(duì)列的確以先進(jìn)先出(FIFO)的順序執(zhí)行任務(wù):第一個(gè)入隊(duì)列的任務(wù)就會(huì)被第一個(gè)執(zhí)行。但是并發(fā)隊(duì)列不會(huì)等待前一個(gè)任務(wù)結(jié)束之后才開(kāi)始提交下一個(gè)任務(wù)晤愧,所以下一個(gè)任務(wù)緊接著也提交了大莫,之后的任務(wù)也一樣。
第一次看的時(shí)候可能不太明白官份,其實(shí)隊(duì)列還是并發(fā)的提交到的只厘,只是在它們真正準(zhǔn)備好執(zhí)行的時(shí)候烙丛,必須一個(gè)一個(gè)的放到串行隊(duì)列里面執(zhí)行(譯者理解)。
但是我們知道一個(gè)隊(duì)列里面的任務(wù)實(shí)際上不是在這個(gè)隊(duì)列中執(zhí)行的羔味,而是把準(zhǔn)備好執(zhí)行的任務(wù)重入到它的目標(biāo)隊(duì)列中執(zhí)行河咽。我們把一個(gè)串行隊(duì)列作為并發(fā)隊(duì)列的目標(biāo)隊(duì),它將會(huì)把隊(duì)列以先進(jìn)先出的順序在串行隊(duì)列中執(zhí)行赋元。因?yàn)樵诖嘘?duì)列中執(zhí)行任務(wù)必須等到它前一個(gè)任務(wù)執(zhí)行結(jié)束忘蟹,原來(lái)在并行隊(duì)列中運(yùn)行的任務(wù)并強(qiáng)制串行化。本質(zhì)上說(shuō)搁凸,一個(gè)串行的目標(biāo)隊(duì)列可以串行化一個(gè)并行的隊(duì)列媚值。(英文的特點(diǎn)是可以把內(nèi)容講的很細(xì),所以有的時(shí)候看英文的可能更好理解一些护糖。)
house1Queue 把 partyLine 隊(duì)列作為其目標(biāo)隊(duì)列褥芒,而 partyLine 會(huì)把默認(rèn)優(yōu)先級(jí)全局的并行隊(duì)列作為其目標(biāo)隊(duì)列。所以在 house1Queue 的任務(wù)會(huì)被重入到 partyLine 隊(duì)列椅文,最后被加入全局并行隊(duì)列執(zhí)行喂很。
缺乏對(duì)目標(biāo)隊(duì)列的認(rèn)知,容易造成死循環(huán)的麻煩皆刺,比如不小心設(shè)置了一系列的目標(biāo)隊(duì)列最后可能指向最開(kāi)始的那個(gè)隊(duì)列少辣。最終造成不可預(yù)測(cè)的后果,所以不要這么做羡蛾。
多個(gè)隊(duì)列設(shè)置同一個(gè)目標(biāo)隊(duì)列
多個(gè)隊(duì)列可以設(shè)置同一個(gè)隊(duì)列作為目標(biāo)隊(duì)列漓帅。房間二的人也想打電話(huà)給房間一的人,所以為他們創(chuàng)建一個(gè)并發(fā)隊(duì)列痴怨,也把 partyLine 隊(duì)列作為它的目標(biāo)隊(duì)列忙干。
// 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;
}
運(yùn)行程序,你觀(guān)察到了什么浪藻?
因?yàn)閮蓚€(gè)并發(fā)隊(duì)列都是同一個(gè)串行隊(duì)列捐迫,來(lái)自?xún)蓚€(gè)隊(duì)列的任務(wù)必須等待前一個(gè)任務(wù)執(zhí)行完畢。把一個(gè)串行隊(duì)列作為目標(biāo)隊(duì)列可以串行化來(lái)自?xún)蓚€(gè)并行隊(duì)列的任務(wù)爱葵。
移除一個(gè)或者兩個(gè)并行隊(duì)列的目標(biāo)隊(duì)列施戴,看看會(huì)發(fā)生什么?
兩頭開(kāi)始同時(shí)打電話(huà)萌丈,不等待電話(huà)掛斷赞哗,下一個(gè)人有開(kāi)始打電話(huà),會(huì)完全錯(cuò)亂辆雾。
真實(shí)世界的目標(biāo)隊(duì)列
目標(biāo)隊(duì)列可以應(yīng)用在一些優(yōu)雅的設(shè)計(jì)模式中肪笋。上面的例子,我們使用一個(gè)或者多個(gè)并發(fā)隊(duì)列并且使其串行化。使用串行隊(duì)列作為目標(biāo)隊(duì)列表明你在同一時(shí)刻只想做一件事情藤乙,不管有多少獨(dú)立的線(xiàn)程在競(jìng)爭(zhēng)資源猜揪。這「一件事」可以是讀取數(shù)據(jù)庫(kù),操作物理磁盤(pán)驅(qū)動(dòng)湾盒,或者操作一些硬件資源湿右。
設(shè)置并發(fā)隊(duì)列的目標(biāo)隊(duì)列為串行隊(duì)列可能回造成死鎖,如果某個(gè)任務(wù)必須再并發(fā)隊(duì)列中執(zhí)行的話(huà)罚勾。所以小心使用這種模式。
當(dāng)你想要協(xié)調(diào)不同來(lái)源的異步事件時(shí)吭狡,串行目標(biāo)隊(duì)列就顯得非常的重要尖殃。例如:定時(shí)器,網(wǎng)絡(luò)事件划煮,文件系統(tǒng)的活動(dòng)等送丰。當(dāng)你想要協(xié)調(diào)來(lái)自不同框架的對(duì)象,或者不能修改源碼弛秋,這顯得非常的有用器躏。我們會(huì)在后面的文章講到定時(shí)器和事件源。
正如我的同事 Mike E. 所說(shuō)蟹略,將串行隊(duì)列設(shè)置為并發(fā)隊(duì)列的目標(biāo)沒(méi)有真實(shí)的使用場(chǎng)景登失。我表示同意:我們很難找到一個(gè)例子,設(shè)置并發(fā)的目標(biāo)隊(duì)列到串行隊(duì)列上優(yōu)于直接使用
dispach_async
提交到串行隊(duì)列上挖炬。
并發(fā)隊(duì)列給了你不一樣的魔力:你可以讓任務(wù)按照其本來(lái)的方式執(zhí)行揽浙,直到你加入一個(gè)障礙 block。如果你這么做意敛,會(huì)使得所有已經(jīng)入隊(duì)了的任務(wù)被暫停馅巷,直到當(dāng)前已經(jīng)開(kāi)始執(zhí)行的任務(wù)以及障礙任務(wù)執(zhí)行完畢,它們才會(huì)繼續(xù)執(zhí)行草姻。就像按下多條流水下的總暫停開(kāi)關(guān)钓猬,在重新啟動(dòng)開(kāi)關(guān)之前你可以做一些想做的事情。
最后
到這里目標(biāo)隊(duì)列的內(nèi)容就講完了撩独。我想如果你剛開(kāi)始接觸 GCD 的話(huà)敞曹,這些知識(shí)可能會(huì)有點(diǎn)難。事實(shí)上跌榔,你可以繼續(xù)幸敢煅悖快樂(lè)的前行不用管 GCD 的目標(biāo)隊(duì)列。如果有一天你遇到一個(gè)難題僧须,可以用目標(biāo)隊(duì)列來(lái)優(yōu)雅的解決的話(huà)纲刀,你會(huì)認(rèn)為學(xué)習(xí)這些是值得的。
我希望學(xué)習(xí)這些你是快樂(lè)的,下次相見(jiàn)示绊,我們會(huì)講講如何使用 GCD 來(lái)實(shí)現(xiàn)設(shè)計(jì)類(lèi)锭部。