GCD死锁

死锁

1、定义:

所谓死锁,通常指有两个线程T1和T2都卡住了,并等待对方完成某些操作。T1不能完成是因为它在等待T2完成。但T2也不能完成,因为它在等待T1完成。于是大家都完不成,就导致了死锁(DeadLock)。

2、产生死锁的条件:

产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。

3、图示:

image.png

首先来看一份导致死锁的典型代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_sync(dispatch_get_main_queue(), ^(void){
            NSLog(@"这里死锁了");
        });
    }
    return 0;
}

比如这个最简单的OC命令行程序就会导致死锁,运行后不会看到任何结果。

基本概念

在解释为什么会死锁之前,首先明确一下“同步&异步”“串行&并发”这两组基本概念:

1、同步&异步

1.1、同步

同步执行:比如这里的dispatch_sync,这个函数会把一个block加入到指定的队列中,而且会一直等到执行完blcok,这个函数才返回。因此在block执行完之前,调用dispatch_sync方法的线程是阻塞的。

1.2、异步

异步执行:一般使用dispatch_async,这个函数也会把一个block加入到指定的队列中,但是和同步执行不同的是,这个函数把block加入队列后不等block的执行就立刻返回了。

1.3、关于这GCD两个函数的强调:

dispatch_async 和 dispatch_sync 他们的作用是将 任务(block)添加进指定的队列中。并根据是否为sync决定调用该函数的线程是否需要阻塞
注意:这里调用该函数的线程并不执行 参数中指定的任务(block块),任务的执行者是GCD分配给任务所在队列的线程
结论:调用dispatch_sync和dispatch_async的线程,并不一定是任务(block块)的执行者

2、串行&并发(队列)

2.1、串行队列

串行队列:比如这里的dispatch_get_main_queue。这个队列中所有任务,一定按照FIFO(先来后到的顺序)执行。不仅如此,还可以保证在执行某个任务时,在它前面进入队列的所有任务肯定执行完了。对于每一个不同的串行队列,系统会为这个队列建立唯一的线程来执行代码

2.2、并发队列

比如使用dispatch_get_global_queue。这个队列中的任务也是按照FIFO(先来后到的顺序)开始执行,注意是开始,但是它们的执行结束时间是不确定的,取决于每个任务的耗时。并发队列中的任务:GCD会动态分配多条线程来执行。具体几条线程取决于当前内存使用状况,线程池中线程数等因素。

3、分析

回到上面的死锁代码中:首先明确的是:执行这个dispatch_get_main_queue队列的是主线程。执行了dispatch_sync函数后,将block添加到了main_queue中,同时调用dispatch_syn这个函数的线程(也就是主线程)被阻塞,等待block执行完成,而执行主线程队列任务的线程正是主线程,此时他处于阻塞状态,所以block永远不会被执行,因此主线程一直处于阻塞状态。因此这段代码运行后,并非卡在block中无法返回,而是根本无法执行到这个block

4、总结:

我们思考一下,什么情况下会导致死锁:采用排除法:即先看看什么情况下不会发生死锁。(先来讨论异步情况,再讨论同步情况。)
4.1 异步执行block肯定不会发生死锁。比如刚刚的代码改成这样:

dispatch_async(dispatch_get_global_queue(0,0), ^(void){
NSLog(@"这就不死锁了");
});

甚至可以总结出来:异步执行一定不会导致死锁。因为回顾一下之前导致的死锁的原因,很重要的一点是主线程在执行dispatch_sync,这是个同步方法,block执行完之前都不会返回。而既然是异步的执行,那么是立刻返回的,因此不会阻塞主线程。双向的阻塞不成立了,只是主线程处理blcok时阻塞,但这不会引起死锁。
所以接下来就只需要重点思考一下,在同步执行时,什么时候会导致死锁。
4.2.1: 可以再得出一个结论,同步的向 并发队列 中添加block不会导致死锁
原因如下:之前由于在串行队列中添加了block,block一直等到前面的任务处理完才会执行,从而导致了死锁。如果采用同步的向并发队列中添加block,首先需要明确的一点是同步方式是不会再开线程的,也就是说当前的调用线程会去立即执行block,直到block执行完成后才会继续向下执行。但是因为是并发队列,队列中下一个任务的执行不需要等待上一个任务的完成,所以即使添加到当前调用任务的队列也不会产生死锁,当前线程会立即执行新添加的任务,然后返回,并继续向下执行。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"11111====current thread :%@",[NSThread currentThread]);
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"222====current thread :%@",[NSThread currentThread]);
        });
        NSLog(@"333333333");
    });

结果:
[10324:5277752] 11111====current thread :<NSThread: 0x60400027f3c0>{number = 3, name = (null)}
[10324:5277752] 222====current thread :<NSThread: 0x60400027f3c0>{number = 3, name = (null)}
[10324:5277752] 333333333
我们可以看到,并发队列中:同一条线程在上一个任务没有完成的情况下,可以去执行下一个任务。因为并发队列中下一个任务的执行不需要上一个任务执行完成。

4.2.2: 最后讨论下同步的向同步队列中添加block,这种情况下会不会造成死锁呢,答案是:不一定。
比如文章开头的例子就属于这种情况。如果同步的向另外一个串行队列添加方法,并不一定导致死锁。比如:

dispatch_queue_t queue = dispatch_queue_create("SimaSDK", nil);
dispatch_sync(queue, ^(void){
NSLog(@"这个也不会死锁");
});

因为:SimaSDK这个队列的执行线程是主线程,同步方式不会开辟新线程,但这是我们是将任务添加到了SimaSDK这个队列中,所以主线程会来立即执行这个队列中的任务,执行完成后就会返回,主线程不会继续被阻塞。所以不会死锁。

事实上,导致死锁的原因一定是:

在某一个串行队列中,同步的向这个队列添加block。
同步的向串行队列中添加

另外,因为队列是可以嵌套的,比如在A队列(串行)添加一个任务a,在a这个任务中向B队列(串行)添加任务b,在b这个任务中又向A队列添加任务,这就间接满足了“在某一个串行队列中,同步的向这个队列添加block”。但是我们好像每一次都没有直接向相同的队列中添加block。

所以:判断是否发生死锁的最好方法就是看有没有在串行队列(当然也包括主队列)中向这个队列添加任务

我们使用同步的方法编程,往往是要求保证任务之间的执行顺序是完全确定的。且不说GCD提供了很多强大的功能来满足这个需求,向串行队列中同步的添加任务本身就是不合理的,毕竟队列已经是串行的了,直接异步添加就可以了。

推荐阅读更多精彩内容