GCD知识补充.目标队列+GCD循环

闲逛看一下GCD的一个实用的功能:目标队列(target queues)。

学习目标队列之前,我们先看下一种特殊的队列:全局并发队列(the global concurrent queues)。

全局并发队列(Global concurrent queues)

GCD给我们的程序提供了4种全局并发队列。这些队列非常特殊,因为它们是由库自动创建的,永远不会被阻塞的(dispatch_barrier_async 对全局队列无效),并且它们处理障碍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_PRIORITY_DEFAULT的全局并发队列。

拥有一个目标队列对一个普通的自定义队列来说有什么意义呢?答案可能有点令人意外:自定义队列里每一个准备好要执行的block,将会被重新加入到这个队列的目标队列里去执行。

但是等一下,我们不是一直假设block就在其所在的队列里执行吗?难道这都是骗人的吗?

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

你的队列继承了其目标队列的优先级。将你的队列的目标队列改为更高或更低优先级的全局并发队列,能有效的改变你的队列的优先级。

只有全局并发队列和主队列才能执行block。其他所有的队列最终都必须设置其中一个为它的目标队列。

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

dispatch_set_target_queue(Queen1, Queen2);

把Queen1的block放到Queen2中进行执行, 可以把一个并发队列变成串行队列.

目标队列实践

让我们来看个例子。

几代人以前,我们很多人的祖父母家的电话都被连接到了一个共用线路。这是在一个社区的所有电话都连接到一个单回路的布置,任何一个人拿起电话就能听见其他正在打电话的人在说什么。

假设我们有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(并发队列)的目标队列是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;
}

运行这段程序,发现了什么?

由于2个并发队列的目标队列设置为了同一个串行队列,所以2个并发队列中的block将会被一个接一个的执行。一个串行队列串行化了以其为目标队列的2个并发队列。

将其中一个或全部队列的目标队列移除,看看会发生什么。结果在你意料之中吗?

又变成了没有等上一次通话结束,新的电话就被接通了。不在串行化了, 发生了抢占现象。

目标队列的实际应用

目标队列可以应用在一些优雅的设计中。在上面的例子中,我们用了一个或多个并发队列并且串行化了它们的执行操作。设定一个串行队列为目标队列也就表明了,不管有多少不同的线程在竞争资源,同一时间只做一件事。这个“一件事”可能是一个数据库请求,访问物理磁盘驱动,或者操作一些硬件资源。

如果有一些block必须被并发执行程序才能继续运行,那么给一个并发队列设置一个串行目标队列,可能会造成死锁。要谨慎使用这种模式。

当你想要协调不同来源的异步时间时,串行目标队列是很重要的,比如计时器,网络时间,文件系统等等。当你需要协调一些来自不同框架的对象的事件时,或者你不能更改一个类的源代码时,串行目标队列也会相当有用。在以后的文章中我会谈一谈计时器和其他一些事件源。

正如我的同事Mike E.所说的:把一个串行队列设置为一个并发队列的目标队列并没有实际的应用的意义。我倾向于他的观点:我很难找到一个例子,设置并发队列的目标队列为串行队列要优于直接dispatch_async到一个串行队列上。

并发目标队列给你另一种魔力:你可以让block以它们原来的方式继续执行,除非你入队了一个障碍block(barrier block)。如果你这样做了,将会使得所有入队的block暂停执行,直到当前正在执行的blcok和障碍block执行完毕再恢复执行。这就像多条操作流的一个总开关,你可以在恢复执行前做一些其他的工作。


通常我们会用for循环遍历,但是GCD给我们提供了快速迭代的函数dispatch_apply

dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。

如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。可这样就体现不出快速迭代的意义了。
我们可以利用并发队列进行异步执行。比如说遍历 0~10 这10个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。
还有一点,无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"dispatch_apply 开始");
    dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t i) {
        NSLog(@" %zu  %@",i,[NSThread currentThread]);
        sleep(2);
    });
    NSLog(@"dispatch_apply 结束");
    
}

因为是在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定, 本来需要20S的循环在6s内完成了。

但是apply-结束一定在最后执行。这是因为dispatch_apply函数会等待全部任务执行完毕。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值