原文发表于humancode.us,地址是GCD Target Queues,由我们团队的井小二童鞋(美女哦)翻译完成。该文已得到原文作者的翻译许可。
这是关于GCD系列的第四篇文章。
跟我一起放慢脚步,看一下GCD的一个实用功能:目标队列(target queues)。
开始旅程之前,我们先来学习下一个特殊的队列:全局并发队列。
全局并发队列
GCD提供了四种在程序中一直有效的全局并发队列。这些队列非常特殊,因为他们由系统自动创建,能够一直运行,并且能够像处理普通的block一样处理barrier block。因为这些队列是并发的,所以所有入队的block将并行运行。
这四种全局并发队列有不同的优先级:
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
- DISPATCH_QUEUE_PRIORITY_BACKGROUND
高优先级队列中的block会抢占低优先级队列中的block的资源。
这些全局并发队列在GCD中扮演了线程优先级的角色。像线程一样,高优先级队列中执行的block可能会消耗所有CPU的资源,而使低优先级的队列却处于“饥饿”状态,并阻止了入队低优先级队列的block的执行。
通过下面的方法,你能得到一个全局并发队列:
dispatch_queue_t defaultPriorityGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
目标队列
那么如何开始使用这些全局并发队列呢?令人惊讶的答案是:你已经在使用它们!你创建的任何队列都包含一个目标队列。默认情况下,这些队列的目标队列是优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT
的全局队列。
一个队列有一个目标队列是什么意思呢?事实上答案有些让人惊讶:每一次一个入队的block准备执行时,队列会将这个block重新放入目标队列来实际执行这个block。
但是等一下,我们不是已经假设了block是在其所在的队列上执行么?难道一切都是假象?
不是的。由于所有新的队列都是以默认优先级的全局并发队列为目标队列的,所以我们队列中任何准备执行的block基本上都会立即执行。除非你改变队列的目标队列,否则block看上去是“运行在你的队列上”。
你的队列继承了目标队列的属性。将队列的目标队列设置为较高或较低优先级的全局并发队列中的一个,将会改变你的队列的优先级。
只有全局并发队列和主队列才能执行block。所有其他的队列都必须以这两种队列中的一种为目标队列。
任务队列实用专场
让我们来看个例子。
几代人以前,我们很多人的祖父母家的电话都被接成到共用电话线路(party lines)。共用电话线路是一种协议,一个社区的所有电话都被接到单一回路中,任何一个接起电话的人都能听到其他线上的人正在说什么。
让我们假设有两组人,住在两所房子里,连接了共用电话线路: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;
}
运行这段程序,看看发生了什么:
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...
真是简直了。。。没有等上一次通话结束,新的通话就被立刻连接了。让我们看看是否能解决这个问题。创建一个串行队列,并将它设置为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;
}
结果如下:
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.
好多了吧。
这种方式可能并不会立即展示,因为并发队列实际上按照FIFO(先进先出)的顺序执行入队的block:最先入队的block会最先被执行。但是并发队列不会等待一个block执行完成才开始后续的block,所以后续入队的block是并发开始的,依此类推。
然而我们了解到,队列实际上并不执行它自己的block,而是将准备执行的block重新放入它的目标队列中。当你将一个串行队列设置为并发队列的目标队列时,它将以FIFO的顺序将其所有的block放入串行队列来执行。由于串行队列在前一个block结束运行之前并不会执行新的block,原来在并发队列中的block将被迫以串行方式运行。总的来说,就是串行的目标队列能够序列化并发队列。
house1Queue以partyLine为目标队列,而partyLine又以默认优先级的全局并发队列为目标队列。这样,house1Queue中的block会被重新放入partyLine队列中,然后放入全局并发队列,最终在全局并发队列中执行。
理论上,创建一个死循环的目标队列是可行的,在设置一序列的目标队列后又可能重新指向原始队列。这么做的后果是不确定的,所以不要这样做。
一对多的目标队列
多个队列能指向相同的目标队列。在房间2中的人们也希望打电话给房间1中的人,为他们创建一个队列,将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;
}
运行这段程序,发现了什么?
由于这两个并发队列指向了同一个串行队列,两个并发队列中的block必须一个接一个的执行。单个队列将两个并发队列的block序列化了。
移除一个或两个并发队列对目标队列指向,看看发生了什么。实际跟你的预期一致么?
目标队列的实际应用
目标队列能应用于一些优雅的设计模式中。在上面的例子中,我们创建了一个或多个并发队列,并序列化了他们的运行。指定一个串行队列作为目标队列,其实核心思想就是说,不管有多少独立的线程在竞争资源,同一时刻我们只做一件事。这个“一件事”可以是数据库请求,访问物理磁盘驱动,或者操作一些硬件资源。
如果程序中有blocks必须并发执行时,设置一个并发队列指向一个串行队列,可能会引发死锁。所以谨慎使用此模式。
当你需要协调不同来源的异步事件时,例如定时器,网络事件,文件系统等等,串行目标队列就变得很重要了。当你需要协调不同框架,不同对象的事件时,或者你不能更改类的源代码时,串行任务队列也非常有用。我会在后续的文章中介绍定时器和其他事件源。
正如我的同事Mike E指出的那样:将并发队列指向串行队列,没有现实的应用意义。我表示同意:我很难找到一个列子,说将一个串行队列设置为一个并行队列的目标队列这种做法优于直接dispatch_async到一个串行队列上。
并发任务队列赋予了你不同的魔力:block都能按照它们固有的方式完美执行,直到一个barrier block入队。如果一旦入队了一个barrier block,会阻止所有入队的未执行的block执行,直到所有当前正在运行的block执行完毕,以及barrier block完全执行完毕。这就很像在几条业务流中点击了控制暂停的按钮,于是你就能在恢复执行之前做些其他事情。
最后
这里结束我们对目标队列的探索。我知道如果你是刚开始接触GCD,可能需要多花些精力。实际上,即使不了解目标队列,你也能做很多事情。但是有一天,你会发现,你能够很优雅的通过目标队列来解决一些至关重要的开发问题,而为此学习目标队列的知识也是值得的。
希望这篇文章是让人愉快的旅途。下期我会讲述与GCD完美配合的类模式设计,下期见。