【译】GCD目标队列(GCD Target Queues)

这是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座房子里,house1Folkshouse2Folks,他们连接到了一个共用线路上。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队列的目标队列是partyLinepartyLine队列的目标队列是默认优先级的全局并发队列,所以,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更好的工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容

  • 本文翻译自GCD Target Queues,个人觉得写得很不错,准备翻译一遍,查了一下却发现已经有前辈翻译过了,...
    SmallflyBlog阅读 578评论 0 1
  • 原文发表于humancode.us,地址是GCD Target Queues,由我们团队的井小二童鞋(美女哦)翻译...
    知识小集阅读 2,393评论 2 15
  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,734评论 1 17
  • 背景 担心了两周的我终于轮到去医院做胃镜检查了!去的时候我都想好了最坏的可能(胃癌),之前在网上查的症状都很相似。...
    Dely阅读 9,226评论 21 42
  • 程序中同步和异步是什么意思?有什么区别? 解释一:异步调用是通过使用单独的线程执行的。原始线程启动异步调用,异步调...
    风继续吹0阅读 1,024评论 1 2