故事背景:
GCD的死鎖問題,一直是在使用多線程的時候,一個比較繞也必須要注意的問題,今天在工作中我們幾個同事又討論到了這個話題,通過和大伙的交流,發(fā)現(xiàn)不少的同事還是有繞不明白的地方, 我就想到,要不我來寫一個關于GCD死鎖的專題好了,于是就有了這篇關于GCD死鎖的專題: (下方的理解,僅限于我個人的理解,有不妥的地方,望大家斧正!)
如果您有任何意見或建議也可以通過郵件或微博聯(lián)系我
環(huán)境信息:
Mac OS X 10.11.3
Xcode 7.2
iOS 9.2
闡述:
1. 什么是GCD ?
GCD帮碰,全稱 Grand Central Dispatch乖订「鞫郑可翻譯為”牛逼的中樞調度器”。它是純C語言的啃洋,提供了非常多強大的函數(shù)。
Grand是宏偉的篡悟、極重要的意思致板。
GCD是提供了功能強大的任務和隊列控制功能,相比于NSOperation更加底層坊夫,雖然現(xiàn)象蘋果極力的推薦使用NSOperation來解決多線程問題, 但是,就目前市場上大部分企業(yè)的iOS開發(fā)團隊而言, GCD仍然還是大頭, NSOperation也只會逐步的來替代GCD, 因此在開線程的時候,如果不注意也會導致一些問題, 比如死鎖砖第。
2.什么是GCD死鎖 ?
所謂死鎖,通常指有兩個線程A和B都卡住了环凿,A在等B ,B在等A,相互等待對方完成某些操作梧兼。A不能完成是因為它在等待B完成。但B也不能完成智听,因為它在等待A完成羽杰。于是大家都完不成,就導致了死鎖(DeadLock)到推。
對于部分新手來說, 可能認為GCD死鎖是很高端的操作系統(tǒng)層面的問題考赛,離我很遠,一般不會遇上莉测。其實這種想法是非常錯誤的颜骤,因為只要簡單三行代碼(如果愿意,甚至寫在一行就可以)就可以人為創(chuàng)造出死鎖的情況,如:
int main(int argc, const char * argv[]) {
@autoreleasepool {
dispatch_sync(dispatch_get_main_queue(), ^(void){
NSLog(@"這里死鎖了");
});
}
return 0;
}
復制代碼
3. 要理解GCD死鎖,必須先理解GCD的幾個概念:
&3.1串行與并行
**串行和并行都是相對于隊列而言的 **
-隊列(負責調度任務)
-串行隊列:一個接一個的調度任務
-并發(fā)隊列:可以同時調度多個任務
在使用GCD的時候捣卤,我們會把需要處理的任務放到Block中忍抽,然后將任務追加到相應的隊列里面,這個隊列董朝,叫做Dispatch Queue梯找。
隊列一般存在于兩種Dispatch Queue,
一種是要等待上一個執(zhí)行完益涧,再執(zhí)行下一個的Serial Dispatch Queue锈锤,這叫做串行隊列;
另一種,則是不需要上一個執(zhí)行完久免,就能執(zhí)行下一個的Concurrent Dispatch Queue浅辙,叫做并行隊列。
這兩種阎姥,均遵循FIFO原則,也就是先進先出原則记舆。
舉一個簡單的例子,在三個任務中輸出1呼巴、2泽腮、3,
串行隊列輸出是有序的1衣赶、2诊赊、3,
但是并行隊列的先后順序就不一定了府瞄。
復制代碼
那么碧磅,并行隊列又是怎么在執(zhí)行呢?
并行隊列雖然可以同時多個任務的處理遵馆,但是并行隊列的處理量鲸郊,還是要根據(jù)當前系統(tǒng)狀態(tài)來。如果當前系統(tǒng)狀態(tài)最多處理2個任務货邓,那么1秆撮、2會排在前面,3什么時候操作换况,就看1或者2誰先完成像吻,然后3接在后面。
串行和并行就簡單說到這里复隆,關于它們的技術點其實還有很多,可以自行了解姆涩。
&3.2 同步與異步
串行與并行針對的是隊列挽拂,而同步與異步,針對的則是線程骨饿。
最大的區(qū)別在于亏栈,同步線程要阻塞當前線程,必須要等待同步線程中的任務執(zhí)行完宏赘,返回以后绒北,才能繼續(xù)執(zhí)行下一任務;而異步線程則是不用等待察署。
僅憑這幾句話還是很難理解闷游,所以可以多準備幾個案例,邊分析邊理解。
&3.3 GCD API
GCD API很多脐往,這里僅介紹本文用到的休吠。
- 系統(tǒng)標準提供的兩個隊列
// 全局隊列,一個特殊的并行隊列
dispatch_get_global_queue
// 主隊列业簿,在主線程中運行瘤礁,因為主線程只有一個,所以這是一個特殊的串行隊列
dispatch_get_main_queue
復制代碼
- 除此之外梅尤,還可以自己生成隊列
// 從DISPATCH_QUEUE_SERIAL看出柜思,這是串行隊列
dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL)
// 同理,這是一個并行隊列
dispatch_queue_create("com.demo.concurrentQueue", DISPATCH_QUEUE_CONCURRENT)
復制代碼
- 同步與異步線程的創(chuàng)建:
dispatch_sync(..., ^(block)) // 同步線程
dispatch_async(..., ^(block)) // 異步線程
復制代碼
案例與分析
假設你已經(jīng)基本了解了上面提到的知識巷燥,接下來進入案例講解階段赡盘。
案例一: 當同步遇到了串行
NSLog(@"1"); // 任務1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2"); // 任務2
});
NSLog(@"3"); // 任務3
復制代碼
控制臺輸出結果:
1
復制代碼
分析:
- dispatch_sync表示是一個同步線程;
- dispatch_get_main_queue表示運行在主線程中的主隊列矾湃;
- 任務2是同步線程的任務亡脑。
- 任務3需要等待任務2結束之后再執(zhí)行.
首先執(zhí)行任務1,這是肯定沒問題的邀跃,只是接下來霉咨,程序遇到了同步線程,那么它會進入等待拍屑,等待任務2執(zhí)行完途戒,然后執(zhí)行任務3。但這是主隊列僵驰,是一個特殊的串行隊列,有任務來喷斋,當然會將任務加到隊尾,然后遵循FIFO原則執(zhí)行任務蒜茴。那么星爪,現(xiàn)在任務2就會被加到最后,任務3排在了任務2前面粉私,問題來了:
任務3要等任務2執(zhí)行完才能執(zhí)行顽腾,任務2又排在任務3后面,意味著任務2要在任務3執(zhí)行完才能執(zhí)行诺核,所以他們進入了互相等待的局面抄肖。【既然這樣窖杀,那干脆就卡在這里吧】這就是死鎖漓摩。
[圖片上傳失敗...(image-220540-1566963152072)]
案例1分析:
案例二:當同步遇到了并行
NSLog(@"1"); // 任務1
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"2"); // 任務2
});
NSLog(@"3"); // 任務3
復制代碼
控制臺輸出結果為:
1
2
3
復制代碼
分析:
首先執(zhí)行任務1,接下來會遇到一個同步線程入客,程序會進入等待管毙。等待任務2執(zhí)行完成以后腿椎,才能繼續(xù)執(zhí)行任務3。從dispatch_get_global_queue可以看出锅风,任務2被加入到了全局的并行隊列中酥诽,當并行隊列執(zhí)行完任務2以后,返回到主隊列皱埠,繼續(xù)執(zhí)行任務3肮帐。
[圖片上傳失敗...(image-2cea82-1566963152072)]
案例2分析:
案例三: 咱們來點復雜一些的: 同步異步都有
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任務1
dispatch_async(queue, ^{
NSLog(@"2"); // 任務2
dispatch_sync(queue, ^{
NSLog(@"3"); // 任務3
});
NSLog(@"4"); // 任務4
});
NSLog(@"5"); // 任務5
復制代碼
控制臺輸出結果:
1
5
2
// 2 和 5 的順序不一定 , 3, 4, 沒有輸出
復制代碼
分析:
這個案例沒有使用系統(tǒng)提供的串行或并行隊列,而是自己通過dispatch_queue_create函數(shù)創(chuàng)建了一個DISPATCH_QUEUE_SERIAL的串行隊列边器。
1.執(zhí)行任務1训枢;
2.遇到異步線程,將【任務2忘巧、同步線程恒界、任務4】加入串行隊列中。因為是異步線程砚嘴,所以在主線程中的任務5不必等待異步線程中的所有任務完成十酣;
3.因為任務5不必等待,所以2和5的輸出順序不能確定际长;
4.任務2執(zhí)行完以后耸采,遇到同步線程工育,這時虾宇,將任務3加入串行隊列;
5.又因為任務4比任務3早加入串行隊列如绸,所以,任務3要等待任務4完成以后搪泳,才能執(zhí)行岸军。但是任務3所在的同步線程會阻塞脏榆,所以任務4必須等任務3執(zhí)行完以后再執(zhí)行。這就又陷入了無限的等待中坞生,造成死鎖又兵。
[圖片上傳失敗...(image-792e13-1566963152072)]
案例3分析:
案例四:異步遇到同步回主線程
NSLog(@"1"); // 任務1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2"); // 任務2
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"3"); // 任務3
});
NSLog(@"4"); // 任務4
});
NSLog(@"5"); // 任務5
復制代碼
控制臺輸出結果:
1
2
5
3
4
// 2 和 5 順序不一定
復制代碼
分析:
這個案例,我相信大家都熟悉,沒錯,這就是典型的異步加載數(shù)據(jù),回調主線程更新UI那個案例;
首先摔认,將【任務1、異步線程电谣、任務5】加入Main Queue中况鸣,異步線程中的任務是:【任務2镐捧、同步線程列牺、任務4】泌辫。
所以震放,先執(zhí)行任務1诈铛,然后將異步線程中的任務加入到Global Queue中幢竹,因為異步線程蜕企,所以任務5不用等待幸乒,結果就是2和5的輸出順序不一定腔召。
然后再看異步線程中的任務執(zhí)行順序。任務2執(zhí)行完以后等恐,遇到同步線程。將同步線程中的任務又回調加入到Main Queue中,這時加入的任務3在任務5的后面撤逢。
當任務3執(zhí)行完以后,沒有了阻塞,程序繼續(xù)執(zhí)行任務4议双。
從以上的分析來看,得到的幾個結果:1最先執(zhí)行;2和5順序不一定幼苛;4一定在3后面墙杯。
[圖片上傳失敗...(image-6bd8db-1566963152072)]
案例4分析:
案例五: 當我們典型案例4,遇到了主線程上出現(xiàn)無限循環(huán)的時候
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1"); // 任務1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2"); // 任務2
});
NSLog(@"3"); // 任務3
});
NSLog(@"4"); // 任務4
while (1) {
}
NSLog(@"5"); // 任務5
復制代碼
打印臺輸出結果:
1
4
// 1 和 4 順序不一定
復制代碼
分析:
和上面幾個案例的分析類似嫉髓,先來看看都有哪些任務加入了Main Queue:【異步線程算行、任務4、死循環(huán)硫狞、任務5】紧唱。
在加入到Global Queue異步線程中的任務有:【任務1、同步線程癣猾、任務3】五芝。
第一個就是異步線程隘擎,任務4不用等待,所以結果任務1和任務4順序不一定蹲姐。
任務4完成后扎阶,程序進入死循環(huán),Main Queue阻塞宰掉。但是加入到Global Queue的異步線程不受影響呵哨,繼續(xù)執(zhí)行任務1后面的同步線程。
同步線程中轨奄,將任務2加入到了主線程孟害,并且,任務3等待任務2完成以后才能執(zhí)行挪拟。這時的主線程挨务,已經(jīng)被死循環(huán)阻塞了。所以任務2無法執(zhí)行玉组,當然任務3也無法執(zhí)行谎柄,在死循環(huán)后的任務5也不會執(zhí)行。
最終惯雳,只能得到1和4順序不定的結果朝巫。
[圖片上傳失敗...(image-f24df9-1566963152072)]
案例5分析:
GCD死鎖問題,我們就暫時討論到這里,上面列舉的5各案例,基本已經(jīng)概括了GCD遇到的絕大多數(shù)情況了,以后有遇到其他情況的時候,再來補充,