GCD同步阻塞原理

GCD因?yàn)楣δ軓?qiáng)大,操作簡(jiǎn)便,成為蘋(píng)果官方推薦使用的多線程API。然而GCD也難只要逃涉及多線程就會(huì)遇到的死鎖問(wèn)題,這里通過(guò)幾個(gè)例子,詳述下GCD死鎖原理。
GCD的死鎖往往發(fā)生在同步執(zhí)行方式中,因?yàn)橹挥型綀?zhí)行方式會(huì)阻塞當(dāng)前線程,等待GCD里的任務(wù)完成,函數(shù)才能繼續(xù)執(zhí)行。但是同步執(zhí)行方式的使用場(chǎng)景很少,因?yàn)橥綀?zhí)行隊(duì)列任務(wù),當(dāng)前線程會(huì)阻塞等待dispatch_sync函數(shù)返回才能繼續(xù)執(zhí)行,而同步執(zhí)行方式也不會(huì)開(kāi)辟新線程,所以即使同步執(zhí)行并行隊(duì)列里的任務(wù),執(zhí)行效果也是將隊(duì)列中的任務(wù)按先后順序依次執(zhí)行,這樣的效果完全就沒(méi)必要使用dispatch_sync函數(shù)了,直接在主線程按順序執(zhí)行也能達(dá)到一樣效果。

這里先介紹下主隊(duì)列概念。主隊(duì)列也是一個(gè)隊(duì)列,只不過(guò)是個(gè)特殊隊(duì)列,已經(jīng)由系統(tǒng)創(chuàng)建好,且是全局唯一的。主隊(duì)列中的任務(wù)都只會(huì)放到主線程中執(zhí)行,所以主隊(duì)列是一個(gè)串行隊(duì)列。需要在主線程里執(zhí)行的任務(wù)都會(huì)被添加到主隊(duì)列,由主線程去依次執(zhí)行。獲取主隊(duì)列的方式是使用函數(shù):dispatch_get_main_queue();



下面開(kāi)始舉例講述同步死鎖原理

1.主線程里同步執(zhí)行主隊(duì)列任務(wù)

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

執(zhí)行效果如下:


圖片.png

發(fā)生了線程崩潰

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

這句提示很經(jīng)典 dispatch_brrier_sync被放到當(dāng)前線程所擁有的隊(duì)列 里執(zhí)行。
引用網(wǎng)上一幅圖說(shuō)明:



當(dāng)調(diào)用dispatch_sync的時(shí)候,它指定了兩個(gè)參數(shù),指定隊(duì)列和指定任務(wù), 所以系統(tǒng)將指定任務(wù)(也就是任務(wù)2)放入到了指定隊(duì)列(也就是dispatch_get_main_queue() )中。執(zhí)行方式是同步執(zhí)行,所以系統(tǒng)會(huì)阻塞,等待dispatch_sync函數(shù)返回,而dispatch_sync函數(shù)又在等任務(wù)2執(zhí)行完才能返回。dispatch_sync指定在主隊(duì)列執(zhí)行,所以任務(wù)2會(huì)被添加到主隊(duì)列執(zhí)行。主隊(duì)列是串行隊(duì)列,隊(duì)列里的任務(wù)必須按添加進(jìn)隊(duì)列的順序依次執(zhí)行。而主隊(duì)列此刻已經(jīng)添加的任務(wù)有:任務(wù)1(已經(jīng)執(zhí)行),dispatch_sync函數(shù)(正在執(zhí)行),任務(wù)3(等待執(zhí)行)。也就是說(shuō),任務(wù)2被添加到主隊(duì)列最后,它要等任務(wù)3執(zhí)行完,它才能執(zhí)行。當(dāng)然,就算沒(méi)有任務(wù)3,線程依然會(huì)卡死,因?yàn)閐ispatch_sync函數(shù)正在執(zhí)行,它比任務(wù)2還是沒(méi)法執(zhí)行。
總結(jié)一下就是dispatch_sync在等任務(wù)2執(zhí)行,而任務(wù)2想執(zhí)行,必須等主隊(duì)列里排在它前面的任務(wù)執(zhí)行完,dispatch_sync函數(shù)調(diào)用和任務(wù)3都完成,它才能執(zhí)行,所以任務(wù)2也在等。他們彼此互相等對(duì)方執(zhí)行,就構(gòu)成了死鎖。
其實(shí)發(fā)生死鎖有兩個(gè)關(guān)鍵點(diǎn),同步執(zhí)行和串行隊(duì)列。只要在一個(gè)隊(duì)列中以同步方式向該隊(duì)列添加任務(wù),就會(huì)產(chǎn)生死鎖。
死鎖的例子跟循環(huán)引用有些類(lèi)似,A持有B,B持有A,A若想釋放,必須等B先釋放,但是B也持有了A,B想釋放的時(shí)候,B又要求A先釋放,彼此互相等對(duì)方釋放。
再次吐槽下,這種情況在開(kāi)發(fā)中幾乎不會(huì)遇到,很明顯,想要實(shí)現(xiàn)的效果是按順序執(zhí)行1,2,3。那么任務(wù)2直接按順序?qū)懢褪橇耍a本身就會(huì)按順序執(zhí)行。何必要放到隊(duì)列里,再以同步方式執(zhí)行。多此一舉,既浪費(fèi)Cpu,還會(huì)因此線程安全隱患。

2.主線程里同步執(zhí)行其他隊(duì)列任務(wù)

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

執(zhí)行效果如下:

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

可以看到這次就能正常順序執(zhí)行下去。
同上次的區(qū)別是這次有兩個(gè)隊(duì)列,主隊(duì)列和全局隊(duì)列。


圖片.png

系統(tǒng)將任務(wù)1,同步執(zhí)行函數(shù)dispatch_sync和任務(wù)3扔到了主線程里,dispatch_sync函數(shù)將任務(wù)2交給全局隊(duì)列去執(zhí)行,然后就阻塞等待任務(wù)2執(zhí)行完畢后返回,全局隊(duì)列拿到任務(wù)后, 就會(huì)將任務(wù)在派發(fā)到線程上去執(zhí)行,任務(wù)不需要等待其他任務(wù)執(zhí)行結(jié)束。
等任務(wù)2執(zhí)行完畢后,dispatch_sync函數(shù)返回,繼續(xù)執(zhí)行任務(wù)3。

3.從阻塞一步步到死鎖

1)并行隊(duì)列

接上面的例子,如果global隊(duì)列里之前有新添加的任務(wù),任務(wù)2會(huì)阻塞住 等待其他任務(wù)執(zhí)行完才能執(zhí)行嗎?答案是不會(huì)的,

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"); // 任務(wù)1
    dispatch_sync(globalqueue, ^{
        NSLog(@"2---%@",[NSThread currentThread]); // 任務(wù)2
    });
    NSLog(@"3"); // 任務(wù)3

執(zhí)行結(jié)果:

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)}

可以看到,任務(wù)3后添加進(jìn)globalqueue隊(duì)列中,卻先執(zhí)行了,并沒(méi)有受先添加進(jìn)隊(duì)列中的任務(wù)阻塞。這是因?yàn)間lobalqueue是并發(fā)隊(duì)列,系統(tǒng)負(fù)責(zé)將隊(duì)列中的任務(wù)依次取出(注意:是依次取出,只要是隊(duì)列,就滿足FIFO原則),然后分發(fā)到各個(gè)線程,至于誰(shuí)先執(zhí)行完,那就看任務(wù)量的大小,以及任務(wù)搶奪CPU的能力了。也算有點(diǎn)拼RP的成分。
那么如果把并行隊(duì)列換成串行隊(duì)列會(huì)發(fā)生什么?

2) 串行隊(duì)列
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"); // 任務(wù)1
    dispatch_sync(seriaquque, ^{
        NSLog(@"2---%@",[NSThread currentThread]); // 任務(wù)2
    });
    NSLog(@"3"); // 任務(wù)3

執(zhí)行結(jié)果

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

可以看到先添加進(jìn)串行隊(duì)列中的任務(wù)執(zhí)行完成,任務(wù)2才能繼續(xù)執(zhí)行。
這就是串行隊(duì)列的特點(diǎn),前一個(gè)任務(wù)執(zhí)行完成,后面任務(wù)才能繼續(xù)執(zhí)行。
如果我們將隊(duì)列中的第一個(gè)任務(wù)換成一個(gè)死循環(huán),例如while,那么第一個(gè)任務(wù)永遠(yuǎn)執(zhí)行不完,第二個(gè)任務(wù)永遠(yuǎn)處于等待狀態(tài),而dispatch_sync也永遠(yuǎn)處于等待狀態(tài),這樣也會(huì)造成跟死鎖一樣的效果,但不一樣的地方是這個(gè)產(chǎn)生的原因不是因?yàn)榫€程互相等待,而是一個(gè)任務(wù)本身是個(gè)死循環(huán),卡住了整個(gè)隊(duì)列的執(zhí)行。


3) 使用信號(hào)量模擬死鎖
    dispatch_queue_t seriaquque = dispatch_queue_create("seriaquque", NULL);    
    //創(chuàng)建信號(hào)量  初始化為0
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    NSLog(@"1"); // 任務(wù)1
    dispatch_sync(seriaquque, ^{
       
        //使用信號(hào)量,并讓信號(hào)量-1,如果當(dāng)前信號(hào)量是0就一直等待,直到信號(hào)量大于0執(zhí)行,也就是說(shuō)當(dāng)執(zhí)行到dispatch_semaphore_signal(semaphore)后,任務(wù)2就會(huì)被執(zhí)行
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        
        NSLog(@"2"); // 任務(wù)2
        
    });
    
    //發(fā)送信號(hào)量,使信號(hào)量加1,這句代碼執(zhí)行后,處于阻塞狀態(tài)的dispatch_semaphore_wait會(huì)收到信號(hào), 執(zhí)行它后面的代碼。
    dispatch_semaphore_signal(semaphore);

執(zhí)行結(jié)果:

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

可以看到,任務(wù)2和任務(wù)3都無(wú)法繼續(xù)執(zhí)行下去了,但區(qū)別與主線程產(chǎn)生死鎖的情況是 此處系統(tǒng)沒(méi)有做異常處理,沒(méi)有 讓代碼直接崩潰。
任務(wù)2需要等dispatch_semaphore_signal(semaphore)函數(shù)執(zhí)行過(guò)后才能繼續(xù)執(zhí)行,而dispatch_semaphore_signal(semaphore)函數(shù)需要等dispatch_sync函數(shù)返回后才能繼續(xù)執(zhí)行,dispatch_sync又需要等它內(nèi)部任務(wù)(也就是任務(wù)2)完成才能繼續(xù)執(zhí)行,他們彼此互相等待,形成了一個(gè)環(huán)路,誰(shuí)都不松口,誰(shuí)也執(zhí)行不了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容