美文网首页iOS开发实用技术java多线程相关
iOS中GCD产生死锁原因分析及解决方案

iOS中GCD产生死锁原因分析及解决方案

作者: 好雨知时节浩宇 | 来源:发表于2017-07-10 19:22 被阅读1697次

    死锁

    1、定义:

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

    2、产生死锁的条件:

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

    3、图示:

    死锁图示

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

    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提供了很多强大的功能来满足这个需求,向串行队列中同步的添加任务本身就是不合理的,毕竟队列已经是串行的了,直接异步添加就可以了。

    转载注明出处:
    iOS中GCD产生死锁原因分析及解决方案

    相关文章

      网友评论

      • d7137e6a1411:不错,跟我的想法一样,在该串行队列中同步添加block到该串行队列才会造成死锁
      • 落寞King:仍然没有明白 串行加同步不堵塞主线程,而主队列加同步就堵塞主线程(这个我明白).两者都是在主线程执行的任务.为何前者没有堵塞线程.还请帮忙解答下..困惑.谢谢.
        好雨知时节浩宇:同步方式添加一个任务到串行队列中,首先,同步方式不会额外开辟线程,由当前线程去执行添加到串行队列中的任务(这里以上面SimaSDK代码段为例)。其次:因为SimaSDK是一个普通的串行队列,主线程调用该代码段后,由于是同步方式,这时主线程队列中的任务被阻塞。主线程转而去执行simaSDK队列中的任务,执行完成后。回到主线程队列继续执行身下的任务。没有构成死锁条件。所以主线程不会被阻塞。
      • 落寞King:最后讨论下同步的向同步队列中添加block,这种情况下会不会造成死锁呢,答案是:不一定。
        应该是串行队列吧
      • hopestar199005:你确定同步向并发队列派发任务会开启新线程?
        好雨知时节浩宇:@hopestar199005 共同进步
        hopestar199005:@好雨知时节浩宇 我也是实验之后才发现 共同进步~
        好雨知时节浩宇:多谢提醒,是我理解有误,同步情况下不会开辟新线程,文章已经修改,还请校阅,十分感谢指正
      • Mr_0:很精彩

      本文标题:iOS中GCD产生死锁原因分析及解决方案

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