GCD同步阻塞原理

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)完成才能继续执行,他们彼此互相等待,形成了一个环路,谁都不松口,谁也执行不了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容