这是GCD介绍的第四篇文章。
跟我一起"闲逛"一会,看一下GCD的一个实用的功能:目标队列(target queues)。
开启旅程之前,我们先学习一种特殊的队列:全局并发队列(the global concurrent queues)。
全局并发队列(Global concurrent queues)
GCD给我们的程序提供了4种全局并发队列。这些队列非常特殊,因为它们是由库自动创建的,永远不会被阻塞的,并且它们处理障碍block和一般的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看起来就是在你的队列中执行的。
你的队列继承了其目标队列的优先级。将你的队列的目标队列改为更高或更低优先级的全局并发队列,能有效的改变你的队列的优先级。
<p>
只有全局并发队列和主队列才能执行block。其他所有的队列最终都必须设置其中一个为它的目标队列。
目标队列实践
让我们来看个例子。
几代人以前,我们很多人的祖父母家的电话都被连接到了一个共用线路。这是在一个社区的所有电话都连接到一个单回路的布置,任何一个人拿起电话就能听见其他正在打电话的人在说什么。
假设我们有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;
}
运行这段程序看看会发生什么:
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.
好多了!
可能不会被马上发现,并发队列里的block以先进先出(FIFO)的顺序被执行,也就是说先入队的block将会被先执行。但是一个并发队列里的block并不会等待前一个block执行完毕才会开始执行,之后的block应该一起开始执行。
我们知道一个队列里的block实际上并不是在这个队列上运行的,而是把准备好要执行的block重新入队到其目标队列里去执行。当你把一个并发队列的目标队列设置为一个串行队列时,这个并发队列就会把其上的block以先进先出的顺序入队到那个串行队列中,也就是其目标队列。又因为串行队列里的block必须等待其前一个block执行完毕才会开始执行,所以那些最开始入队到并发队列的block将被迫以串行的方式执行。总的来说,串行目标队列能够串行化一个并发队列。
house1Queue
队列的目标队列是partyLine
,partyLine
队列的目标队列是默认优先级的全局并发队列,所以,house1Queue
上的block会被重新入队到partyLine
队列,然后再被入队到全局并发队列并执行。
<p>
设置一堆目标队列有可能产生一个循环,使你的目标队列最终指向最开始的那个队列。这样做会产生不可预知的后果,所以别这么做。
多个队列设置同一个目标队列
多个队列可以设置同一个队列为其目标队列。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;
}
运行这段程序,发现了什么?
由于2个并发队列的目标队列设置为了同一个串行队列,所以2个并发队列中的block将会被一个接一个的执行。一个串行队列串行化了以其为目标队列的2个并发队列。
将其中一个或全部队列的目标队列移除,看看会发生什么。结果在你意料之中吗?
目标队列的实际应用
目标队列可以应用在一些优雅的设计中。在上面的例子中,我们用了一个或多个并发队列并且串行化了它们的执行操作。设定一个串行队列为目标队列也就表明了,不管有多少不同的线程在竞争资源,同一时间只做一件事。这个“一件事”可能是一个数据库请求,访问物理磁盘驱动,或者操作一些硬件资源。
如果有一些block必须被并发执行程序才能继续运行,那么给一个并发队列设置一个串行目标队列,可能会造成死锁。要谨慎使用这种模式。
当你想要协调不同来源的异步时间时,串行目标队列是很重要的,比如计时器,网络时间,文件系统等等。当你需要协调一些来自不同框架的对象的事件时,或者你不能更改一个类的源代码时,串行目标队列也会相当有用。在以后的文章中我会谈一谈计时器和其他一些事件源。
正如我的同事Mike E.所说的:把一个串行队列设置为一个并发队列的目标队列并没有实际的应用的意义。我倾向于他的观点:我很难找到一个例子,设置并发队列的目标队列为串行队列要优于直接
dispatch_async
到一个串行队列上。
并发目标队列给你另一种魔力:你可以让block以它们原来的方式继续执行,除非你入队了一个障碍block(barrier block)。如果你这样做了,将会使得所有入队的block暂停执行,直到当前正在执行的blcok和障碍block执行完毕再恢复执行。这就像多条操作流的一个总开关,你可以在恢复执行前做一些其他的工作。
最后
到这里,目标队列也说的差不多了。如果你刚开始接触GCD,我知道这些内容对你来说短时间内可能有点难消化。实际上,你完全可以继续快乐地走在原来的学习路线上,不用停下来去了解目标队列。但是如果有一天,你被一个问题困扰,你突然发现这个问题可以用目标队列的方式优雅的解决,那么我们的这次"闲逛"就值得了。
我希望你能享受这次"闲逛"。下次再见,我将会谈一谈如何设计类,使其能够配合GCD更好的工作。