iOS死鎖原理和解決方案

原文鏈接 - 徹底搞懂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呜投。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末加匈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子仑荐,更是在濱河造成了極大的恐慌雕拼,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粘招,死亡現(xiàn)場(chǎng)離奇詭異啥寇,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門辑甜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)衰絮,“玉大人,你說(shuō)我怎么就攤上這事磷醋∶担” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵邓线,是天一觀的道長(zhǎng)淌友。 經(jīng)常有香客問(wèn)我,道長(zhǎng)骇陈,這世上最難降的妖魔是什么亩进? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮缩歪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谍憔。我一直安慰自己匪蝙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布习贫。 她就那樣靜靜地躺著逛球,像睡著了一般。 火紅的嫁衣襯著肌膚如雪苫昌。 梳的紋絲不亂的頭發(fā)上颤绕,一...
    開(kāi)封第一講書(shū)人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音祟身,去河邊找鬼奥务。 笑死,一個(gè)胖子當(dāng)著我的面吹牛袜硫,可吹牛的內(nèi)容都是我干的氯葬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼婉陷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼帚称!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起秽澳,我...
    開(kāi)封第一講書(shū)人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤闯睹,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后担神,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體楼吃,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了所刀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衙荐。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浮创,靈堂內(nèi)的尸體忽然破棺而出忧吟,到底是詐尸還是另有隱情,我是刑警寧澤斩披,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布溜族,位于F島的核電站,受9級(jí)特大地震影響垦沉,放射性物質(zhì)發(fā)生泄漏煌抒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一厕倍、第九天 我趴在偏房一處隱蔽的房頂上張望寡壮。 院中可真熱鬧,春花似錦讹弯、人聲如沸况既。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)棒仍。三九已至,卻和暖如春臭胜,著一層夾襖步出監(jiān)牢的瞬間莫其,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工耸三, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留乱陡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓仪壮,卻偏偏與公主長(zhǎng)得像蛋褥,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子睛驳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

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