原文鏈接 - 徹底搞懂OC中GCD導(dǎo)致死鎖的原因和解決方案
GCD提供了功能強(qiáng)大的任務(wù)和隊(duì)列控制功能嵌削,相比于NSOperationQueue更加底層,因此如果不注意也會(huì)導(dǎo)致死鎖俯逾。
所謂死鎖察蹲,通常指有兩個(gè)線程A和B都卡住了,并等待對(duì)方完成某些操作遏暴。A不能完成是因?yàn)樗诘却鼴完成贡耽。但B也不能完成衷模,因?yàn)樗诘却鼳完成。于是大家都完不成蒲赂,就導(dǎo)致了死鎖(DeadLock)阱冶。
有一定GCD使用經(jīng)驗(yàn)的新手通常認(rèn)為,死鎖是很高端的操作系統(tǒng)層面的問(wèn)題滥嘴,離我很遠(yuǎn)木蹬,一般不會(huì)遇上。其實(shí)這種想法是非常錯(cuò)誤的若皱,因?yàn)橹灰?jiǎn)單三行代碼(如果愿意镊叁,甚至寫(xiě)在一行就可以)就可以人為創(chuàng)造出死鎖的情況。
int main(int argc, const char * argv[]) {
@autoreleasepool {
dispatch_sync(dispatch_get_main_queue(), ^(void){
NSLog(@"這里死鎖了");
});
}
return 0;
}
比如這個(gè)最簡(jiǎn)單的OC命令行程序就會(huì)導(dǎo)致死鎖走触,運(yùn)行后不會(huì)看到任何結(jié)果晦譬。
在解釋為什么會(huì)死鎖之前,首先明確一下“同步&異步”“串行&并發(fā)”這兩組基本概念:
同步執(zhí)行:比如這里的dispatch_sync互广,這個(gè)函數(shù)會(huì)把一個(gè)block加入到指定的隊(duì)列中敛腌,而且會(huì)一直等到執(zhí)行完blcok,這個(gè)函數(shù)才返回惫皱。因此在block執(zhí)行完之前像樊,調(diào)用dispatch_sync方法的線程是阻塞的。
與之對(duì)應(yīng)的就有“異步執(zhí)行”的概念:
異步執(zhí)行:一般使用dispatch_async旅敷,這個(gè)函數(shù)也會(huì)把一個(gè)block加入到指定的隊(duì)列中凶硅,但是和同步執(zhí)行不同的是,這個(gè)函數(shù)把block加入隊(duì)列后不等block的執(zhí)行就立刻返回了扫皱。
接下來(lái)看一看另一組相對(duì)的概念:“串行&并發(fā)”
串行隊(duì)列:比如這里的dispatch_get_main_queue足绅。這個(gè)隊(duì)列中所有任務(wù),一定按照先來(lái)后到的順序執(zhí)行韩脑。不僅如此氢妈,還可以保證在執(zhí)行某個(gè)任務(wù)時(shí),在它前面進(jìn)入隊(duì)列的所有任務(wù)肯定執(zhí)行完了段多。對(duì)于每一個(gè)不同的串行隊(duì)列首量,系統(tǒng)會(huì)為這個(gè)隊(duì)列建立唯一的線程來(lái)執(zhí)行代碼。
與之相對(duì)的是并發(fā)隊(duì)列:
并發(fā)隊(duì)列:比如使用dispatch_get_global_queue进苍。這個(gè)隊(duì)列中的任務(wù)也是按照先來(lái)后到的順序開(kāi)始執(zhí)行加缘,注意是開(kāi)始,但是它們的執(zhí)行結(jié)束時(shí)間是不確定的觉啊,取決于每個(gè)任務(wù)的耗時(shí)拣宏。對(duì)于n個(gè)并發(fā)隊(duì)列,GCD不會(huì)創(chuàng)建對(duì)應(yīng)的n個(gè)線程而是進(jìn)行適當(dāng)?shù)膬?yōu)化
我們把整個(gè)dispatch_sync看作是一個(gè)任務(wù)杠人,比如說(shuō)是非常關(guān)鍵勋乾、需要高度集中注意力的運(yùn)鈔過(guò)程。這個(gè)過(guò)程非常重要嗡善,一旦開(kāi)始執(zhí)行就必須一氣呵成辑莫,任何事情都不能干擾這個(gè)過(guò)程(阻塞線程)。
現(xiàn)在主線程開(kāi)始執(zhí)行這個(gè)運(yùn)鈔任務(wù)罩引,任務(wù)執(zhí)行到一半時(shí)各吨,突然運(yùn)鈔員說(shuō)我好累啊,辛苦了好久了袁铐,我現(xiàn)在需要休息(向主線程添加了block)揭蜒。運(yùn)鈔員天真的認(rèn)為,我知道運(yùn)鈔這個(gè)事很重要昭躺,本來(lái)應(yīng)該等到運(yùn)鈔結(jié)束后再休息(這樣是串行)忌锯。但是在這之前,我的身體條件不允許工作领炫。
但是之前已經(jīng)說(shuō)了偶垮,運(yùn)鈔這件事很重要,它一旦開(kāi)始就不能結(jié)束(阻塞線程)帝洪。怎么能允許有人中途休息呢似舵,因此要休息可以(block是可以執(zhí)行的),先把鈔票運(yùn)到安全地方再休息葱峡。
對(duì)應(yīng)到代碼里面來(lái)砚哗,當(dāng)我們想要同步執(zhí)行這個(gè)block的時(shí)候,其實(shí)是告訴主線程砰奕,你把事情處理完了蛛芥,就過(guò)來(lái)處理我這個(gè)blcok提鸟,在此之前我一直等你。而主線程呢仅淑,剛處理dispatch_sync函數(shù)到一半呢称勋,這個(gè)函數(shù)還沒(méi)返回,哪里有空去執(zhí)行block涯竟。因此這段代碼運(yùn)行后赡鲜,并非卡在block中無(wú)法返回,而是根本無(wú)法執(zhí)行到這個(gè)block庐船。
好了银酬,總結(jié)一下,到底什么是死鎖筐钟。首先揩瞪,雖然剛剛我們提到了隊(duì)列和線程,以及它們之間的對(duì)應(yīng)關(guān)系盗棵,但是死鎖一定是針對(duì)線程而言的壮韭,隊(duì)列只是GCD給出的抽象數(shù)據(jù)結(jié)構(gòu)。所謂的死鎖纹因,一定是發(fā)生在一個(gè)或多個(gè)線程之間的喷屋。那么死鎖和線程阻塞的關(guān)系呢,可以這么理解瞭恰,雙向的阻塞導(dǎo)致了死鎖屯曹。因?yàn)樽枞蔷€程中經(jīng)常發(fā)生的事情,最多就是主線程的阻塞影響了用戶體驗(yàn)惊畏。而一旦出現(xiàn)了雙向的阻塞恶耽,就導(dǎo)致了死鎖。我們可以看到颜启,主線程是串行的偷俭,在執(zhí)行某一個(gè)任務(wù)的時(shí)候線程被阻塞了,而這個(gè)任務(wù)(dispatch_sync)在執(zhí)行時(shí)缰盏,又要求阻塞主線程涌萤,從而導(dǎo)致了互相的阻塞,也就是死鎖口猜。
接下來(lái)我們思考一下负溪,什么情況下會(huì)導(dǎo)致死鎖。這個(gè)問(wèn)題可能一下子難以得出準(zhǔn)確的回答济炎,為了解決這個(gè)問(wèn)題川抡,我打算使用排除法。即先看看什么情況下不會(huì)發(fā)生死鎖须尚。比如說(shuō)崖堤,異步執(zhí)行block肯定不會(huì)發(fā)生死鎖侍咱。比如剛剛的代碼改成這樣:
dispatch_async(dispatch_get_global_queue(0,0), ^(void){
NSLog(@"這就不死鎖了");
});
甚至可以總結(jié)出來(lái):異步執(zhí)行一定不會(huì)導(dǎo)致死鎖。因?yàn)榛仡櫼幌轮皩?dǎo)致的死鎖的原因密幔,很重要的一點(diǎn)是主線程在執(zhí)行dispatch_sync放坏,這是個(gè)同步方法,block執(zhí)行完之前都不會(huì)返回老玛。而既然是異步的執(zhí)行,那么是立刻返回的钧敞,因此不會(huì)阻塞主線程蜡豹。雙向的阻塞不成立了,只是主線程處理blcok時(shí)阻塞溉苛,但這不會(huì)引起死鎖镜廉。
根據(jù)之前我們的分析和總結(jié),GCD中我們需要關(guān)心的就是同步還是異步執(zhí)行愚战,以及把block添加到哪個(gè)隊(duì)列中(串行還是并發(fā))娇唯。
所以接下來(lái)就只需要重點(diǎn)思考一下,在同步執(zhí)行時(shí)寂玲,什么時(shí)候會(huì)導(dǎo)致死鎖塔插。可以再得出一個(gè)結(jié)論拓哟,向并發(fā)隊(duì)列中添加block不會(huì)導(dǎo)致死鎖想许。再次回顧一下之前導(dǎo)致的死鎖的原因,由于在串行隊(duì)列中添加了block断序,block一直等到前面的任務(wù)處理完才會(huì)執(zhí)行流纹,從而導(dǎo)致了死鎖。現(xiàn)在即使是同步的向并發(fā)隊(duì)列中添加block违诗,GCD會(huì)自動(dòng)為我們管理線程漱凝,主線程目前阻塞著(處理這個(gè)同步方法),那就新建一個(gè)新的線程诸迟,但無(wú)論如何這個(gè)被添加block遲早都會(huì)被執(zhí)行茸炒。而所有添加的block被執(zhí)行完后,同步方法也就返回了亮蒋。因此不會(huì)導(dǎo)致死鎖扣典。
最后再來(lái)討論一下用同步方法向串行隊(duì)列添加block的情況,這種情況下會(huì)不會(huì)造成死鎖呢慎玖,答案是不一定贮尖。事實(shí)上,導(dǎo)致死鎖的原因一定是:
在某一個(gè)串行隊(duì)列中趁怔,同步的向這個(gè)隊(duì)列添加block湿硝。
比如文章開(kāi)頭的例子就屬于這種情況薪前。如果同步的向另外一個(gè)串行隊(duì)列添加方法,并不一定導(dǎo)致死鎖关斜。比如:
dispatch_queue_t queue = dispatch_queue_create("serial", nil);
dispatch_sync(queue, ^(void){
NSLog(@"這個(gè)也不會(huì)死鎖");
});
分析一下代碼示括,向名為serial的串行隊(duì)列添加任務(wù)后,GCD自動(dòng)創(chuàng)建了一個(gè)新的線程痢畜,在這個(gè)線程中執(zhí)行block方法垛膝。在這個(gè)過(guò)程中,主線程和新的線程都是阻塞的丁稀,但是并不會(huì)導(dǎo)致死鎖吼拥。
為什么說(shuō)向另一個(gè)串行隊(duì)列添加任務(wù)不一定導(dǎo)致死鎖呢,因?yàn)殛?duì)列是可以嵌套的线衫,比如在A隊(duì)列(串行)添加一個(gè)任務(wù)a凿可,在a這個(gè)任務(wù)中向B隊(duì)列(串行)添加任務(wù)b,在b這個(gè)任務(wù)中又向A隊(duì)列添加任務(wù)授账,這就間接滿足了“在某一個(gè)串行隊(duì)列中枯跑,同步的向這個(gè)隊(duì)列添加block”。但是我們好像每一次都沒(méi)有直接向相同的隊(duì)列中添加block白热。
所以判斷是否發(fā)生死鎖的最好方法就是看有沒(méi)有在串行隊(duì)列(當(dāng)然也包括主隊(duì)列)中向這個(gè)隊(duì)列添加任務(wù)敛助。又因?yàn)槲覀冎烂總€(gè)串行隊(duì)列對(duì)應(yīng)一個(gè)線程,所以只要不在某個(gè)線程中調(diào)用會(huì)阻塞這個(gè)線程的方法即可棘捣。
事實(shí)上辜腺,我們使用同步的方法編程,往往是要求保證任務(wù)之間的執(zhí)行順序是完全確定的乍恐。且不說(shuō)GCD提供了很多強(qiáng)大的功能來(lái)滿足這個(gè)需求评疗,向串行隊(duì)列中同步的添加任務(wù)本身就是不合理的,畢竟隊(duì)列已經(jīng)是串行的了茵烈,直接異步添加就可以了啊百匆。所以,解決文章開(kāi)頭那個(gè)死鎖例子的最簡(jiǎn)單的方法就是在合適的位置添加一個(gè)字母a呜投。