iOS死锁原理和解决方案

原文链接 - 彻底搞懂OC中GCD导致死锁的原因和解决方案

GCD提供了功能强大的任务和队列控制功能,相比于NSOperationQueue更加底层,因此如果不注意也会导致死锁。

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

有一定GCD使用经验的新手通常认为,死锁是很高端的操作系统层面的问题,离我很远,一般不会遇上。其实这种想法是非常错误的,因为只要简单三行代码(如果愿意,甚至写在一行就可以)就可以人为创造出死锁的情况。

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

比如这个最简单的OC命令行程序就会导致死锁,运行后不会看到任何结果。
在解释为什么会死锁之前,首先明确一下“同步&异步”“串行&并发”这两组基本概念:

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

与之对应的就有“异步执行”的概念:

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

接下来看一看另一组相对的概念:“串行&并发”

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

与之相对的是并发队列:

并发队列:比如使用dispatch_get_global_queue。这个队列中的任务也是按照先来后到的顺序开始执行,注意是开始,但是它们的执行结束时间是不确定的,取决于每个任务的耗时。对于n个并发队列,GCD不会创建对应的n个线程而是进行适当的优化

我们把整个dispatch_sync看作是一个任务,比如说是非常关键、需要高度集中注意力的运钞过程。这个过程非常重要,一旦开始执行就必须一气呵成,任何事情都不能干扰这个过程(阻塞线程)。

现在主线程开始执行这个运钞任务,任务执行到一半时,突然运钞员说我好累啊,辛苦了好久了,我现在需要休息(向主线程添加了block)。运钞员天真的认为,我知道运钞这个事很重要,本来应该等到运钞结束后再休息(这样是串行)。但是在这之前,我的身体条件不允许工作。

但是之前已经说了,运钞这件事很重要,它一旦开始就不能结束(阻塞线程)。怎么能允许有人中途休息呢,因此要休息可以(block是可以执行的),先把钞票运到安全地方再休息。

对应到代码里面来,当我们想要同步执行这个block的时候,其实是告诉主线程,你把事情处理完了,就过来处理我这个blcok,在此之前我一直等你。而主线程呢,刚处理dispatch_sync函数到一半呢,这个函数还没返回,哪里有空去执行block。因此这段代码运行后,并非卡在block中无法返回,而是根本无法执行到这个block。

好了,总结一下,到底什么是死锁。首先,虽然刚刚我们提到了队列和线程,以及它们之间的对应关系,但是死锁一定是针对线程而言的,队列只是GCD给出的抽象数据结构。所谓的死锁,一定是发生在一个或多个线程之间的。那么死锁和线程阻塞的关系呢,可以这么理解,双向的阻塞导致了死锁。因为阻塞是线程中经常发生的事情,最多就是主线程的阻塞影响了用户体验。而一旦出现了双向的阻塞,就导致了死锁。我们可以看到,主线程是串行的,在执行某一个任务的时候线程被阻塞了,而这个任务(dispatch_sync)在执行时,又要求阻塞主线程,从而导致了互相的阻塞,也就是死锁。

接下来我们思考一下,什么情况下会导致死锁。这个问题可能一下子难以得出准确的回答,为了解决这个问题,我打算使用排除法。即先看看什么情况下不会发生死锁。比如说,异步执行block肯定不会发生死锁。比如刚刚的代码改成这样:

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

甚至可以总结出来:异步执行一定不会导致死锁。因为回顾一下之前导致的死锁的原因,很重要的一点是主线程在执行dispatch_sync,这是个同步方法,block执行完之前都不会返回。而既然是异步的执行,那么是立刻返回的,因此不会阻塞主线程。双向的阻塞不成立了,只是主线程处理blcok时阻塞,但这不会引起死锁。

根据之前我们的分析和总结,GCD中我们需要关心的就是同步还是异步执行,以及把block添加到哪个队列中(串行还是并发)。

所以接下来就只需要重点思考一下,在同步执行时,什么时候会导致死锁。可以再得出一个结论,向并发队列中添加block不会导致死锁。再次回顾一下之前导致的死锁的原因,由于在串行队列中添加了block,block一直等到前面的任务处理完才会执行,从而导致了死锁。现在即使是同步的向并发队列中添加block,GCD会自动为我们管理线程,主线程目前阻塞着(处理这个同步方法),那就新建一个新的线程,但无论如何这个被添加block迟早都会被执行。而所有添加的block被执行完后,同步方法也就返回了。因此不会导致死锁。

最后再来讨论一下用同步方法向串行队列添加block的情况,这种情况下会不会造成死锁呢,答案是不一定。事实上,导致死锁的原因一定是:

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

比如文章开头的例子就属于这种情况。如果同步的向另外一个串行队列添加方法,并不一定导致死锁。比如:

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

分析一下代码,向名为serial的串行队列添加任务后,GCD自动创建了一个新的线程,在这个线程中执行block方法。在这个过程中,主线程和新的线程都是阻塞的,但是并不会导致死锁。

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

所以判断是否发生死锁的最好方法就是看有没有在串行队列(当然也包括主队列)中向这个队列添加任务。又因为我们知道每个串行队列对应一个线程,所以只要不在某个线程中调用会阻塞这个线程的方法即可。

事实上,我们使用同步的方法编程,往往是要求保证任务之间的执行顺序是完全确定的。且不说GCD提供了很多强大的功能来满足这个需求,向串行队列中同步的添加任务本身就是不合理的,毕竟队列已经是串行的了,直接异步添加就可以了啊。所以,解决文章开头那个死锁例子的最简单的方法就是在合适的位置添加一个字母a。

你可能感兴趣的:(iOS死锁原理和解决方案)