iOS八股文(十一)多线程之GCD

1,914 阅读6分钟

iOS开发中,无法避免都要使用到GCD(Grand Central Dispatch),我们只需要把待执行的任务放到适合的Dispatch Queue 中,GCD就能帮我们把任务放进合适的线程中执行,而我们并不用管理和操作线程的生命周期,使用起来非常方便。本文通过面试题由浅入深一探开发常用GCD的源码实现。

经典面试题

看面试题之前我们先复习两个单词:

单词解释
serial顺序排列的,连续的;连续播放的,连载的;串行的
concurrent并存的,同时发生的
题目一

以下代码输出结果是什么?

-(void)interview01 {
    dispatch_queue_t queue = dispatch_queue_create("com.demo.queue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"test____1"); // 任务1
    dispatch_async(queue, ^{
        NSLog(@"test____2"); // 任务2
        dispatch_sync(queue, ^{
            NSLog(@"test____3"); // 任务3
        });
        NSLog(@"test____4"); // 任务4
    });
    NSLog(@"test____5"); // 任务5
}

先运行一下,看真实的输出:

image.png

可以看到输出结果为:1⃣️5⃣️2⃣️3⃣️4⃣️。 如果你正好回答到这个答案,那么恭喜你,你答对了。但回答的并不完美。我们先来分析系统输出的答案。

首先这段代码是在主线程执行的,那么在主线程执行的内容我们可以分为3个部分:

image.png 任务1⃣️肯定是最先执行的,然后去执行第2块内容,最后执行任务5⃣️,再继续看第2块使用异步函数配合并发队列,那么disptch里面的block将会在一个新的线程执行。同样的在这个线程里面也可以将执行的代码分成3个部分:

image.png 在这块代码里面任务2⃣️会被先执行,然后再看dispatch_syns,同步函数,那么都不需要看是什么对列,任务3⃣️将会次之执行,最后就是执行任务4⃣️。

剩下的就是需要考虑任务5⃣️和任务2⃣️3⃣️4⃣️的执行顺序了。正确答案其实是他们之间没有顺序,他们在不同的线程执行,理论上讲是可以同时执行的。而之所以打印出来任务2⃣️3⃣️4⃣️在任务5⃣️之后,其实是在执行任务2⃣️3⃣️4⃣️之前,需要创建其执行的线程,创建线程需要消耗微小的时间,还有一方面原因是这块代码在主线程运行的,任务5⃣️在主线程执行,效率也会更高。

这道题考查的是GCD中同步、异步函数和串行、并行队列的使用,使用NSLog来模拟任务执行,但正真的环境中任务的耗时程度是不一样的。

我们假设任务5⃣️是一个耗时20微秒的任务,那么打印结果会不会不一样呢?

image.png 在任务5⃣️之前usleep 20微秒模拟耗时任务。

注意:usleepsleep的区别只是单位不一样,usleep的单位是微秒,sleep的单位是秒。

image.png 可以看到这时候顺序变成了1⃣️2⃣️5⃣️3⃣️4⃣️,正如我们分析结果,5⃣️和2⃣️3⃣️4⃣️是没有顺序的。如果200微妙,运行结果还会是不一样的。

所以这道题面试题的正确答案是:先执行1⃣️,5⃣️在1⃣️后执行,2⃣️3⃣️4⃣️顺序执行,5⃣️和2⃣️3⃣️4⃣️是没有顺序。

题目二

以下代码输出结果是什么?

-(void)interview01 {
    dispatch_queue_t queue = dispatch_queue_create("com.demo.queue", DISPATCH_QUEUE_SERIAL);
    NSLog(@"test____1"); // 任务1
    dispatch_async(queue, ^{
        NSLog(@"test____2"); // 任务2
        dispatch_sync(queue, ^{
            NSLog(@"test____3"); // 任务3
        });
        NSLog(@"test____4"); // 任务4
    });
    NSLog(@"test____5"); // 任务5
}

相对于题目一,这题就没那么老六了。

直接上结果:

image.png 可以看到是崩溃,奔溃原因很简单:使用同步函数当前 串行队列中添加任务,会产生死锁。注意三个关键字,GCD发生死锁的充分条件。

同步异步和串行并发队列的搭配

同步异步和串行并发队列的组合情况如下:

串行队列并发队列
同步函数可能死锁不开启新线程
异步函数开启新线程开启新线程

在组合的时候,还需要考虑的因素还有当前代码的执行是在哪个队列之中,之前也有讲到:使用同步函数当前 串行队列中添加任务,会产生死锁,如果在别的串行队列去添加任务是不会产生死锁的。例如:

- (void)interview02 {
    dispatch_queue_t queue = dispatch_queue_create("com.demo.serial", DISPATCH_QUEUE_SERIAL);
    NSLog(@"---任务1");
    dispatch_sync(queue, ^{
        NSLog(@"---任务2");
    });
    NSLog(@"---任务3");
}

这段代码就是同步函数和串行队列的时候,但不会造成死锁(在主线程中执行)。

串行队列和并发队列的源码解析

在我们开发中,使用队列的时候,苹果给我们给了3个获取队列的api。

- (void)test01 {
    //主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //全局并发队列
    dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
    //自己创建的串行队列
    dispatch_queue_t normalQueue = dispatch_queue_create("com.demo.serial", DISPATCH_QUEUE_SERIAL);
    NSLog(@"%@",mainQueue);
    NSLog(@"%@",globQueue);
    NSLog(@"%@",normalQueue);
}

我们打开dispatch源码找到dispatch_queue_create的api实现部分,来一探究竟。

image.png 可以看到是调用_dispatch_lane_create_with_target并添加2个默认参数实现的,找到对应实现。

image.png

可以看到这个方法里面,把我们传入的DISPATCH_QUEUE_SERIAL或者DISPATCH_QUEUE_CONCURRENT参数进行封装,封装成了dqai。我们可以大致看看封装的实现:

image.png

注意看这里我们可以获取2个有用的点,dqai里面有个dqai_concurrent的属性,顾名思义是代表是否是并发,那么默认的就是串行。

我们在继续看如何根据dqai创建队列的。

image.png 可以看到通过init方法初始化,第三个参数,如果是并发传入DISPATCH_QUEUE_WIDTH_MAX,如果是串行传入1

image.png 而这里是DISPATCH_QUEUE_WIDTH_MAX的定义,可以计算其结果是14

我们再看init函数内部实现

image.png

可以看到如果width最后变成了DQF_WIDTH(width)

接下来我们看看主队列的实现:

image.png

可以看到需要用到一个宏定义的函数,并且传入了2个参数,其中_dispatch_main_q是全局变量,定义如下:

image.png 这里我们再次看到了DQF_WIDTH(1)

根据源码里面的信息,我们可以得知,串行队列和并发队列最根本的区别就是DQF_WIDTH不同,串行队列的为1。这个width可以抽象的理解为队列出口的宽度。可以把串行队列想成一个单向单车道,把任务想成一辆辆车子,车子通过的时候必须一辆一辆按顺序通过;而并发队列可以想成单向多车道,有多个出口,车子可以并行通过。

当然也可以通过下图理解:

image.png

参考链接

iOS GCD源码浅析

iOS底层原理(七)多线程(上