iOS底层学习 - 多线程之GCD初探

2,297 阅读7分钟

通过上一个章节,我们已经了解了一些多线程的基本知识。本章就来讲解在平时开发中,最为常用的GCD的一些知识

系列文章传送门:

iOS底层学习 - 多线程之基础原理篇

GCD简介

定义

GCD全程为Grand Central Dispatch,由C语言实现,是苹果为多核的并行运算提出的解决方案,CGD会自动利用更多的CPU内核,自动管理线程的生命周期,程序员只需要告诉GCD需要执行的任务,无需编写任何管理线程的代码。GCD也是iOS使用频率最高的多线程技术。

优势

  • GCD 是苹果公司为多核的并行运算提出的解决方案
  • GCD 会自动利用更多的CPU内核(比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

GCD使用

GCD的使用API较为简单,主要分为同步和异步执行。在上一章我们对概念已经有所了解,就不做赘述了。

同步执行API

//queue:队列   block:任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

异步执行API

//queue:队列   block:任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

我们发现在使用GCD时,需要传入两个参数。分别是dispatch_queue_t代表添加的队列类型,dispatch_block_t为需要执行的任务,下面我们来看一下不同参数的运行效果。

dispatch_block_t任务

通过查看其定义可得,该参数其实就是一个没有参数的block回调,用来执行任务的。

typedef void (^dispatch_block_t)(void);

dispatch_queue_t队列

dispatch_queue_t参数表示队列的类型。根据上一章节我们知道,队列(FIFO)在iOS中主要一下分为4种:

  1. 主队列(main_queue):由系统创建的串行队列。
    • 获取方式:dispatch_get_main_queue()
  2. 全局队列(global_queue):由系统创建的并发队列
    • 获取方式:dispatch_get_global_queue(long identifier, unsigned long flags);
  3. 串行队列(Serial Dispatch Queue):自定义的串行队列
    • 获取方式:dispatch_queue_create(@"队列名",DISPATCH_QUEUE_SERIAL)
  4. 并发队列(Concurrent Dispatch Queue):自定义的并发队列
    • 获取方式:dispatch_queue_create(@"队列名",DISPATCH_QUEUE_CONCURRENT);

其中,全局队列根据入参的不同,会获取到不同的队列,在日常的开发中,我们一般入参0,来获取到主并发队列。

自定义的队列都是调用dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr)方法来进行创建,两个参数分别为自定义队列名称队列类型。其中串行队列DISPATCH_QUEUE_SERIAL为宏定义的NULL,所以传NULL也表示为串行队列。

现在,我们来看这2种执行模式,3种队列是相互搭配是什么效果

同步 +(主队列或者自定义串行队列)

相关代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1:%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"任务2",[NSThread currentThread]);
    });
    NSLog(@"任务3:%@",[NSThread currentThread]);
}

运行结果

打印出任务1后,程序死锁崩溃

为什么会造成以上的结果呢?

首先分析一下代码:主线程执行完任务1后,在主队列dispatch_get_main_queue()中同步执行(dispatch_sync)任务2,然后执行任务3。

队列的特点是FIFO,主队列中已经存在任务viewDidLoad,往主队列加入任务2,就需要执行完viewDidLoad才能执行任务2。但是想要执行完viewDidLoad又必须先执行viewDidLoad内的任务2和任务3。这就造成了死锁

异步 +(主队列或者自定义串行队列)

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1:%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"任务2:%@",[NSThread currentThread]);
    });
    NSLog(@"任务3:%@",[NSThread currentThread]);
}

运行结果

我们可以看到当获取到主队列后,使用异步执行的方式,不会造成程序的崩溃。

因为主线程执行任务1之后,需要异步(dispatch_async)执行任务2;而dispatch_async不要求立马在当前线程同步执行任务,也就是不会堵塞;所以主线程接着执行任务3,最后异步执行任务2。

同步+(全局队列或者自定义并发队列)

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_sync(queue, ^{
        for (int i = 0; i<3; i++) {
            NSLog(@"任务一:%@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        for (int i = 0; i<3; i++) {
            NSLog(@"任务二:%@",[NSThread currentThread]);
        }
    });
}

运行结果

我们在全局队列中同步执行任务1和任务2,通过打印可以看出,同步执行是按顺序执行任务,执行完任务1再执行任务2。并且打印当前线程,同步执行是在主线程中执行任务,没有开启新线程。且由于队列是并发的,并不会阻塞主线程。

异步+(全局队列或者自定义并发队列)

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i<3; i++) {
            NSLog(@"任务一:%@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i<3; i++) {
            NSLog(@"任务二:%@",[NSThread currentThread]);
        }
    });
}

运行结果:

可以看出任务1和任务2交错执行,并非同步执行那样执行完任务1再执行任务2。而且通过线程编号可以看出,的确开启了新的线程。说明异步执行具备开启新线程的能力。

但是通过上面异步+主队列的打印结果我们可以发现,在主队列时,异步执行也并没有开启新的线程,而仍然是同一个主线程。说明如果在主队列中异步执行,是不会开启新线程的

原因是因为主队列是串行队列,必须执行完一个任务再执行另一个任务,异步执行只是没有造成堵塞,但是在主队列还是是要一步步走的。

同一串行队列嵌套

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ 
        NSLog(@"任务2");
        dispatch_sync(queue, ^{ 
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

运行结果

任务1,任务5,任务2,然后程序崩溃

这里造成死锁崩溃的原因和上面同步+串行队列的原因一样。由于主队列是串行的,所以代码必然是从上到下依次执行的。

打印完任务1后,执行dispatch_async为异步,不会阻塞线程,但是会加入自定义的串行队列中,所以会执行任务5,接着执行异步代码块中逻辑,打印任务2,接着执行dispatch_sync代码块,但是由于其是同步,串行队列中已经有了dispatch_async任务未执行完毕,此时dispatch_sync阻塞当前线程等待任务3执行,造成了相互等待,所以就死锁崩溃了

不同串行队列嵌套

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("myQueue2", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_sync(queue2, ^{
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

运行结果

任务1,任务5,任务2,任务3,任务4

通过运行我们可以发现,当嵌套执行时,同步异步在不同的串行队列执行,并不会造成死锁崩溃。而是按照串行的顺序,执行代码。这事因为dispatch_sync阻塞的并不是当前的线程,而是其他的线程,所以不会造成死锁等待。

同一并发队列嵌套

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_sync(queue, ^{
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

运行结果:

任务1,任务5,任务2,任务3,任务4

主线程执行任务1之后,需要异步dispatch_async执行任务2;所以先执行主线程的任务5,然后执行任务2;接着需要在并发队列中同步dispatch_sync执行任务3,所以会造成阻塞,等待其执行完成,然后执行并发队列中的任务4。

不同并发队列嵌套

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("myQueue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_sync(queue2, ^{ 
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

运行结果:

任务1,任务5,任务2,任务3,任务4

这个代码的分析和不同串行队列嵌套的类似,创建了新的队列,所以是不会造成当前队列的阻塞的。

总结

  • 同步执行没有开辟线程能力,且代码顺序执行,会产生堵塞
  • 同步执行在同一串行队列时,会造成死锁等待
  • 异步执行具有开辟线程的能力,在并发队列执行时,执行顺序不确定,在串行队列执行时,按照顺序执行,且主队列时不会开辟新线程

参考资料

苹果多线程开源代码

iOS底层原理探索—多线程的本质