美文网首页
GCD同步阻塞原理

GCD同步阻塞原理

作者: nemie | 来源:发表于2017-09-16 22:40 被阅读374次

    GCD因为功能强大,操作简便,成为苹果官方推荐使用的多线程API。然而GCD也难只要逃涉及多线程就会遇到的死锁问题,这里通过几个例子,详述下GCD死锁原理。
    GCD的死锁往往发生在同步执行方式中,因为只有同步执行方式会阻塞当前线程,等待GCD里的任务完成,函数才能继续执行。但是同步执行方式的使用场景很少,因为同步执行队列任务,当前线程会阻塞等待dispatch_sync函数返回才能继续执行,而同步执行方式也不会开辟新线程,所以即使同步执行并行队列里的任务,执行效果也是将队列中的任务按先后顺序依次执行,这样的效果完全就没必要使用dispatch_sync函数了,直接在主线程按顺序执行也能达到一样效果。

    这里先介绍下主队列概念。主队列也是一个队列,只不过是个特殊队列,已经由系统创建好,且是全局唯一的。主队列中的任务都只会放到主线程中执行,所以主队列是一个串行队列。需要在主线程里执行的任务都会被添加到主队列,由主线程去依次执行。获取主队列的方式是使用函数:dispatch_get_main_queue();



    下面开始举例讲述同步死锁原理

    1.主线程里同步执行主队列任务

    1  NSLog(@"1"); // 任务1
    2  dispatch_sync(dispatch_get_main_queue(), ^{
    3     NSLog(@"2"); // 任务2
    4  });
    5  NSLog(@"3"); // 任务3
    

    执行效果如下:


    图片.png

    发生了线程崩溃

    BUG IN CLIENT OF LIBDISPATCH: dispatch_barrier_sync called on queue already owned by current thread
    

    这句提示很经典 dispatch_brrier_sync被放到当前线程所拥有的队列 里执行。
    引用网上一幅图说明:



    当调用dispatch_sync的时候,它指定了两个参数,指定队列和指定任务, 所以系统将指定任务(也就是任务2)放入到了指定队列(也就是dispatch_get_main_queue() )中。执行方式是同步执行,所以系统会阻塞,等待dispatch_sync函数返回,而dispatch_sync函数又在等任务2执行完才能返回。dispatch_sync指定在主队列执行,所以任务2会被添加到主队列执行。主队列是串行队列,队列里的任务必须按添加进队列的顺序依次执行。而主队列此刻已经添加的任务有:任务1(已经执行),dispatch_sync函数(正在执行),任务3(等待执行)。也就是说,任务2被添加到主队列最后,它要等任务3执行完,它才能执行。当然,就算没有任务3,线程依然会卡死,因为dispatch_sync函数正在执行,它比任务2还是没法执行。
    总结一下就是dispatch_sync在等任务2执行,而任务2想执行,必须等主队列里排在它前面的任务执行完,dispatch_sync函数调用和任务3都完成,它才能执行,所以任务2也在等。他们彼此互相等对方执行,就构成了死锁。
    其实发生死锁有两个关键点,同步执行和串行队列。只要在一个队列中以同步方式向该队列添加任务,就会产生死锁。
    死锁的例子跟循环引用有些类似,A持有B,B持有A,A若想释放,必须等B先释放,但是B也持有了A,B想释放的时候,B又要求A先释放,彼此互相等对方释放。
    再次吐槽下,这种情况在开发中几乎不会遇到,很明显,想要实现的效果是按顺序执行1,2,3。那么任务2直接按顺序写就是了,代码本身就会按顺序执行。何必要放到队列里,再以同步方式执行。多此一举,既浪费Cpu,还会因此线程安全隐患。

    2.主线程里同步执行其他队列任务

    NSLog(@"1"); // 任务1
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"2"); // 任务2
    });
    NSLog(@"3"); // 任务3
    

    执行效果如下:

    2017-09-16 16:02:18.133 GCD[915:75427] 1
    2017-09-16 16:02:18.133 GCD[915:75427] 2
    2017-09-16 16:02:18.134 GCD[915:75427] 3
    

    可以看到这次就能正常顺序执行下去。
    同上次的区别是这次有两个队列,主队列和全局队列。


    图片.png

    系统将任务1,同步执行函数dispatch_sync和任务3扔到了主线程里,dispatch_sync函数将任务2交给全局队列去执行,然后就阻塞等待任务2执行完毕后返回,全局队列拿到任务后, 就会将任务在派发到线程上去执行,任务不需要等待其他任务执行结束。
    等任务2执行完毕后,dispatch_sync函数返回,继续执行任务3。

    3.从阻塞一步步到死锁

    1)并行队列

    接上面的例子,如果global队列里之前有新添加的任务,任务2会阻塞住 等待其他任务执行完才能执行吗?答案是不会的,

    dispatch_queue_t seriaquque = dispatch_queue_create("seriaquque", NULL);
        dispatch_queue_t globalqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        dispatch_async(globalqueue, ^{
           
            for (int i = 0; i < 10; i++) {
                
                sleep(1);
                NSLog(@"sleep 1s----%d------%@",i,[NSThread currentThread]);
            }
            
        });
        
        NSLog(@"1"); // 任务1
        dispatch_sync(globalqueue, ^{
            NSLog(@"2---%@",[NSThread currentThread]); // 任务2
        });
        NSLog(@"3"); // 任务3
    

    执行结果:

    2017-09-16 16:33:02.477 GCD[1036:92017] 1
    2017-09-16 16:33:02.477 GCD[1036:92017] 2---<NSThread: 0x608000070480>{number = 1, name = main}
    2017-09-16 16:33:02.477 GCD[1036:92017] 3
    2017-09-16 16:33:03.480 GCD[1036:92060] sleep 1s----0------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:04.484 GCD[1036:92060] sleep 1s----1------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:05.484 GCD[1036:92060] sleep 1s----2------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:06.489 GCD[1036:92060] sleep 1s----3------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:07.492 GCD[1036:92060] sleep 1s----4------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:08.492 GCD[1036:92060] sleep 1s----5------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:09.493 GCD[1036:92060] sleep 1s----6------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:10.498 GCD[1036:92060] sleep 1s----7------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:11.501 GCD[1036:92060] sleep 1s----8------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    2017-09-16 16:33:12.507 GCD[1036:92060] sleep 1s----9------<NSThread: 0x608000263e80>{number = 3, name = (null)}
    

    可以看到,任务3后添加进globalqueue队列中,却先执行了,并没有受先添加进队列中的任务阻塞。这是因为globalqueue是并发队列,系统负责将队列中的任务依次取出(注意:是依次取出,只要是队列,就满足FIFO原则),然后分发到各个线程,至于谁先执行完,那就看任务量的大小,以及任务抢夺CPU的能力了。也算有点拼RP的成分。
    那么如果把并行队列换成串行队列会发生什么?

    2) 串行队列
    dispatch_queue_t seriaquque = dispatch_queue_create("seriaquque", NULL);
        dispatch_queue_t globalqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        dispatch_async(seriaquque, ^{
           
            for (int i = 0; i < 10; i++) {
                
                sleep(1);
                NSLog(@"sleep 1s----%d------%@",i,[NSThread currentThread]);
            }
            
        });
        
        NSLog(@"1"); // 任务1
        dispatch_sync(seriaquque, ^{
            NSLog(@"2---%@",[NSThread currentThread]); // 任务2
        });
        NSLog(@"3"); // 任务3
    

    执行结果

    2017-09-16 16:44:19.899 GCD[1071:98356] 1
    2017-09-16 16:44:20.902 GCD[1071:98443] sleep 1s----0------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:21.904 GCD[1071:98443] sleep 1s----1------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:22.904 GCD[1071:98443] sleep 1s----2------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:23.905 GCD[1071:98443] sleep 1s----3------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:24.905 GCD[1071:98443] sleep 1s----4------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:25.906 GCD[1071:98443] sleep 1s----5------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:26.907 GCD[1071:98443] sleep 1s----6------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:27.907 GCD[1071:98443] sleep 1s----7------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:28.908 GCD[1071:98443] sleep 1s----8------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:29.908 GCD[1071:98443] sleep 1s----9------<NSThread: 0x6080002655c0>{number = 3, name = (null)}
    2017-09-16 16:44:29.909 GCD[1071:98356] 2---<NSThread: 0x60800006a5c0>{number = 1, name = main}
    2017-09-16 16:44:29.909 GCD[1071:98356] 3
    

    可以看到先添加进串行队列中的任务执行完成,任务2才能继续执行。
    这就是串行队列的特点,前一个任务执行完成,后面任务才能继续执行。
    如果我们将队列中的第一个任务换成一个死循环,例如while,那么第一个任务永远执行不完,第二个任务永远处于等待状态,而dispatch_sync也永远处于等待状态,这样也会造成跟死锁一样的效果,但不一样的地方是这个产生的原因不是因为线程互相等待,而是一个任务本身是个死循环,卡住了整个队列的执行。


    3) 使用信号量模拟死锁
        dispatch_queue_t seriaquque = dispatch_queue_create("seriaquque", NULL);    
        //创建信号量  初始化为0
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
        NSLog(@"1"); // 任务1
        dispatch_sync(seriaquque, ^{
           
            //使用信号量,并让信号量-1,如果当前信号量是0就一直等待,直到信号量大于0执行,也就是说当执行到dispatch_semaphore_signal(semaphore)后,任务2就会被执行
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
            NSLog(@"2"); // 任务2
            
        });
        
        //发送信号量,使信号量加1,这句代码执行后,处于阻塞状态的dispatch_semaphore_wait会收到信号, 执行它后面的代码。
        dispatch_semaphore_signal(semaphore);
    

    执行结果:

    2017-09-16 22:28:18.209 GCD[697:20527] 1
    

    可以看到,任务2和任务3都无法继续执行下去了,但区别与主线程产生死锁的情况是 此处系统没有做异常处理,没有 让代码直接崩溃。
    任务2需要等dispatch_semaphore_signal(semaphore)函数执行过后才能继续执行,而dispatch_semaphore_signal(semaphore)函数需要等dispatch_sync函数返回后才能继续执行,dispatch_sync又需要等它内部任务(也就是任务2)完成才能继续执行,他们彼此互相等待,形成了一个环路,谁都不松口,谁也执行不了。

    相关文章

      网友评论

          本文标题:GCD同步阻塞原理

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