美文网首页
[译] GCD 系列(四)之—目标队列

[译] GCD 系列(四)之—目标队列

作者: SmallflyBlog | 来源:发表于2016-10-10 21:03 被阅读187次

    本文翻译自GCD Target Queues,个人觉得写得很不错,准备翻译一遍,查了一下却发现已经有前辈翻译过了,不过还是自己自动翻译了一遍,在不改变原意的前提下,做了一些修改,加入一些自己的理解。翻译有参考这篇 GCD目标队列(GCD Target Queues)(侵删)。

    这是 GCD 系列文章 第四篇。

    gcd-logo.png

    我们来看看 GCD 的一个优雅特性:目标队列。

    在开始之前学习一些列队列之前,我们先来看看特殊的一种队列:全局队列。

    全局队列(Global concurrent queues)

    GCD 提供四种可用的并发全局队列,这些队列比较特殊,因为它是由系统库自动创建的,永远不会被阻塞。即使遇到障碍(barrier blocks) 任务,其他任务也不会被阻塞。这些队列都是并发的,所有入队列的任务都会并发执行。

    四种队列对应的优先级分别是:

    • DISPATCH_QUEUE_PRIORITY_HIGH
    • DISPATCH_QUEUE_PRIORITY_DEFAULT
    • DISPATCH_QUEUE_PRIORITY_LOW
    • DISPATCH_QUEUE_PRIORITY_BACKGROUND

    高优先级队列里的任务比低优先级队列里的任务优先执行。

    GCD 的这些全局队列同时也扮演一个线程优先级的角色。像线程一样,执行在高优先级的队列中的任务会抢占 CPU 资源,使得低优先级的队列处于「饥饿」状态,即无法得到执行。

    你可以用如下方式获取全局的并发队列:

    dispatch_queue_t defaultPriorityGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    目标队列(Target Queue)

    这些全局队列该如何使用呢?说出来可能会令你惊讶:你已经在使用它们了。任何队列一旦你创建都会对应有一个目标队列。默认情况下,所有队列(主队列除外)都会把 DISPATCH_QUEUE_PRIORITY_DEFAULT优先级的全局并发队列作为目标队列。

    队列竟然还有目标队列?是不是有点惊讶:实际上每当队列中的任务准备好要执行的时候,队列会把该任务重入(重新放入)到目标队列,真正的执行在目标队列中。

    等等,难道任务不是在我们所提交的队列里面执行的?这些全都是谎言?

    不是的,因为所有自己创建的队列(包括串行队列)都会把默认优先级的全局并发队列当做目标队列,前面说过全局并发队列不会被阻塞,等待工作都是在提交的队列中的,一旦轮到执行,就会被提交到目标队列中,并立刻开始执行。所以除非是你自定义目标队列,否则你完全可以抽象的认为任务就是在你提交的队列中开始执行的。

    你创建的队列的优先级继承自目标全局队列。把目标全局队列的优先级设置的更高或者更低,会同时改变你创建队列的优先级。

    只有全局队列和主队列会执行任务,它们是所有其他队列的目标队列。

    来看一张来自 objc 的图:

    gcd-pool.png

    该图说明了自定义队列分成两路:一路是串行队列进入 GCD 的主队列,另一路是进入 GCD 默认优先级的全局并发队列。最后都在系统维护的线程池中被执行。

    玩转目标队列(Party time with target queues)

    先来看一个列子。

    很久以前,我们的祖先使用公用线路打电话的。一个社区里面的电话都是在一条线路上通信的,任何人只要拿起电话,就可以听到其他人打电话的的声音。

    假设我们有两小组的人,住在两间房子里面,通过单一的线路连接: house1Folks 和 house2Folks。房间一的人喜欢给房间二的人打电话。问题是,他们在打电话前不回去检查是否有其他人在打电话。来看例子:

    // Party line!
    
    #import <Foundation/Foundation.h>
    
    void makeCall(dispatch_queue_t queue, NSString *caller, NSArray *callees) {
        // caller 随机打电话给 callees 里面的一个人
        NSInteger targetIndex = arc4random() % callees.count;
        NSString *callee = callees[targetIndex];
    
        NSLog(@"%@ is calling %@...", caller, callee);
        sleep(1);
        NSLog(@"...%@ is done calling %@.", caller, callee);
    
        // 随机等待一段时间,然后接着打
        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;
    }
    

    运行程序来看看结果:

    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)的顺序执行任务:第一个入队列的任务就会被第一个执行。但是并发队列不会等待前一个任务结束之后才开始提交下一个任务,所以下一个任务紧接着也提交了,之后的任务也一样。

    第一次看的时候可能不太明白,其实队列还是并发的提交到的,只是在它们真正准备好执行的时候,必须一个一个的放到串行队列里面执行(译者理解)。

    但是我们知道一个队列里面的任务实际上不是在这个队列中执行的,而是把准备好执行的任务重入到它的目标队列中执行。我们把一个串行队列作为并发队列的目标队,它将会把队列以先进先出的顺序在串行队列中执行。因为在串行队列中执行任务必须等到它前一个任务执行结束,原来在并行队列中运行的任务并强制串行化。本质上说,一个串行的目标队列可以串行化一个并行的队列。(英文的特点是可以把内容讲的很细,所以有的时候看英文的可能更好理解一些。)

    house1Queue 把 partyLine 队列作为其目标队列,而 partyLine 会把默认优先级全局的并行队列作为其目标队列。所以在 house1Queue 的任务会被重入到 partyLine 队列,最后被加入全局并行队列执行。

    缺乏对目标队列的认知,容易造成死循环的麻烦,比如不小心设置了一系列的目标队列最后可能指向最开始的那个队列。最终造成不可预测的后果,所以不要这么做。

    多个队列设置同一个目标队列

    多个队列可以设置同一个队列作为目标队列。房间二的人也想打电话给房间一的人,所以为他们创建一个并发队列,也把 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;
    }
    

    运行程序,你观察到了什么?

    因为两个并发队列都是同一个串行队列,来自两个队列的任务必须等待前一个任务执行完毕。把一个串行队列作为目标队列可以串行化来自两个并行队列的任务。

    移除一个或者两个并行队列的目标队列,看看会发生什么?

    两头开始同时打电话,不等待电话挂断,下一个人有开始打电话,会完全错乱。

    真实世界的目标队列

    目标队列可以应用在一些优雅的设计模式中。上面的例子,我们使用一个或者多个并发队列并且使其串行化。使用串行队列作为目标队列表明你在同一时刻只想做一件事情,不管有多少独立的线程在竞争资源。这「一件事」可以是读取数据库,操作物理磁盘驱动,或者操作一些硬件资源。

    设置并发队列的目标队列为串行队列可能回造成死锁,如果某个任务必须再并发队列中执行的话。所以小心使用这种模式。

    当你想要协调不同来源的异步事件时,串行目标队列就显得非常的重要。例如:定时器,网络事件,文件系统的活动等。当你想要协调来自不同框架的对象,或者不能修改源码,这显得非常的有用。我们会在后面的文章讲到定时器和事件源。

    正如我的同事 Mike E. 所说,将串行队列设置为并发队列的目标没有真实的使用场景。我表示同意:我们很难找到一个例子,设置并发的目标队列到串行队列上优于直接使用 dispach_async 提交到串行队列上。

    并发队列给了你不一样的魔力:你可以让任务按照其本来的方式执行,直到你加入一个障碍 block。如果你这么做,会使得所有已经入队了的任务被暂停,直到当前已经开始执行的任务以及障碍任务执行完毕,它们才会继续执行。就像按下多条流水下的总暂停开关,在重新启动开关之前你可以做一些想做的事情。

    最后

    到这里目标队列的内容就讲完了。我想如果你刚开始接触 GCD 的话,这些知识可能会有点难。事实上,你可以继续幸福快乐的前行不用管 GCD 的目标队列。如果有一天你遇到一个难题,可以用目标队列来优雅的解决的话,你会认为学习这些是值得的。

    我希望学习这些你是快乐的,下次相见,我们会讲讲如何使用 GCD 来实现设计类。

    相关文章

      网友评论

          本文标题:[译] GCD 系列(四)之—目标队列

          本文链接:https://www.haomeiwen.com/subject/tuevyttx.html