你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS陸哥開發筆記(八) (GCD死鎖及解決方案)

iOS陸哥開發筆記(八) (GCD死鎖及解決方案)

編輯:IOS開發綜合
GCD導致死鎖的原因和解決方案 所謂死鎖,通常指有兩個線程A和B都卡住了,並等待對方完成某些操作。A不能完成是因為它在等待B完成。但B也不能完成,因為它在等待A完成。於是大家都完不成,就導致了死鎖(DeadLock)。

 

在使用GCD的時候,我們會把需要處理的任務放到Block中,然後將任務追加到相應的隊列裡面,這個隊列,叫做Dispatch Queue。然而,存在於兩種Dispatch Queue,一種是要等待上一個執行完,再執行下一個的Serial Dispatch Queue,這叫做串行隊列;另一種,則是不需要上一個執行完,就能執行下一個的Concurrent Dispatch Queue,叫做並行隊列。這兩種,均遵循FIFO原則。

串行與並行針對的是隊列,而同步與異步,針對的則是線程。最大的區別在於,同步線程要阻塞當前線程,必須要等待同步線程中的任務執行完,返回以後,才能繼續執行下一任務;而異步線程則是不用等待。



案例一:

Objective-C   1 2 3 4 5 6 NSLog(@"1");// 任務1 dispatch_sync(dispatch_get_main_queue(),^{ NSLog(@"2");// 任務2 }); NSLog(@"3");// 任務3  

結果,控制台輸出:

Objective-C   1 2 1  

分析:

  1. dispatch_sync表示是一個同步線程;
  2. dispatch_get_main_queue表示運行在主線程中的主隊列;
  3. 任務2是同步線程的任務。

    首先執行任務1,這是肯定沒問題的,只是接下來,程序遇到了同步線程,那麼它會進入等待,等待任務2執行完,然後執行任務3。但這是隊列,有任務來,當然會將任務加到隊尾,然後遵循FIFO原則執行任務。那麼,現在任務2就會被加到最後,任務3排在了任務2前面,問題來了:

    任務3要等任務2執行完才能執行,任務2由排在任務3後面,意味著任務2要在任務3執行完才能執行,所以他們進入了互相等待的局面。【既然這樣,那干脆就卡在這裡吧】這就是死鎖。

    \

     

    案例二:

     

    Objective-C   1 2 3 4 5 6 NSLog(@"1");// 任務1 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0),^{ NSLog(@"2");// 任務2 }); NSLog(@"3");// 任務3  

    結果,控制台輸出:

        Objective-C   1 2 3 4 1 2 3  

    分析:

    首先執行任務1,接下來會遇到一個同步線程,程序會進入等待。等待任務2執行完成以後,才能繼續執行任務3。從dispatch_get_global_queue可以看出,任務2被加入到了全局的並行隊列中,當並行隊列執行完任務2以後,返回到主隊列,繼續執行任務3。

    \

    案例三:

     

    Objective-C   1 2 3 4 5 6 7 8 9 10 11 dispatch_queue_tqueue=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  

    結果,控制台輸出:

        Objective-C   1 2 3 4 5 1 5 2 // 5和2的順序不一定  

    分析:

    這個案例沒有使用系統提供的串行或並行隊列,而是自己通過dispatch_queue_create函數創建了一個DISPATCH_QUEUE_SERIAL的串行隊列。

    1. 執行任務1;
    2. 遇到異步線程,將【任務2、同步線程、任務4】加入串行隊列中。因為是異步線程,所以在主線程中的任務5不必等待異步線程中的所有任務完成;
    3. 因為任務5不必等待,所以2和5的輸出順序不能確定;
    4. 任務2執行完以後,遇到同步線程,這時,將任務3加入串行隊列;
    5. 又因為任務4比任務3早加入串行隊列,所以,任務3要等待任務4完成以後,才能執行。但是任務3所在的同步線程會阻塞,所以任務4必須等任務3執行完以後再執行。這就又陷入了無限的等待中,造成死鎖。

      \

      案例四:

        Objective-C   1 2 3 4 5 6 7 8 9 10 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  

      結果,控制台輸出: 

      Objective-C   1 2 3 4 5 6 7 1 2 5 3 4 // 5和2的順序不一定  

      分析:

      首先,將【任務1、異步線程、任務5】加入Main Queue中,異步線程中的任務是:【任務2、同步線程、任務4】。

      所以,先執行任務1,然後將異步線程中的任務加入到Global Queue中,因為異步線程,所以任務5不用等待,結果就是2和5的輸出順序不一定。

      然後再看異步線程中的任務執行順序。任務2執行完以後,遇到同步線程。將同步線程中的任務加入到Main Queue中,這時加入的任務3在任務5的後面。

      當任務3執行完以後,沒有了阻塞,程序繼續執行任務4。

      從以上的分析來看,得到的幾個結果:1最先執行;2和5順序不一定;4一定在3後面。

      \

      案例五: 

      Objective-C   1 2 3 4 5 6 7 8 9 10 11 12 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   Objective-C   1 結果,控制台輸出: Objective-C   1 2 3 4 1 4 // 1和4的順序不一定  

      分析:

      和上面幾個案例的分析類似,先來看看都有哪些任務加入了Main Queue:【異步線程、任務4、死循環、任務5】。

      在加入到Global Queue異步線程中的任務有:【任務1、同步線程、任務3】。

      第一個就是異步線程,任務4不用等待,所以結果任務1和任務4順序不一定。

      任務4完成後,程序進入死循環,Main Queue阻塞。但是加入到Global Queue的異步線程不受影響,繼續執行任務1後面的同步線程。

      同步線程中,將任務2加入到了主線程,並且,任務3等待任務2完成以後才能執行。這時的主線程,已經被死循環阻塞了。所以任務2無法執行,當然任務3也無法執行,在死循環後的任務5也不會執行。

      最終,只能得到1和4順序不定的結果。

      \


      有一定GCD使用經驗的新手通常認為,死鎖是很高端的操作系統層面的問題,離我很遠,一般不會遇上。其實這種想法是非常錯誤的,因為只要簡單三行代碼(如果願意,甚至寫在一行就可以)就可以人為創造出死鎖的情況。

      intmain(intargc,constchar*argv[]){
      @autoreleasepool{
      dispatch_sync(dispatch_get_main_queue(),^(void){
      NSLog(@"這裡死鎖了");
      });
      }
      return0;
      }

      比如這個最簡單的OC命令行程序就會導致死鎖,運行後不會看到任何結果。

      在解釋為什麼會死鎖之前,首先明確一下“同步&異步”“串行&並發”這兩組基本概念:

      同步執行:比如這裡的dispatch_sync,這個函數會把一個block加入到指定的隊列中,而且會一直等到執行完blcok,這個函數才返回。因此在block執行完之前,調用dispatch_sync方法的線程是阻塞的。

      與之對應的就有“異步執行”的概念:

      異步執行:一般使用dispatch_async,這個函數也會把一個block加入到指定的隊列中,但是和同步執行不同的是,這個函數把block加入隊列後不等block的執行就立刻返回了。

      接下來看一看另一組相對的概念:“串行&並發”

      串行隊列:比如這裡的dispatch_get_main_queue。這個隊列中所有任務,一定按照先來後到的順序執行。不僅如此,還可以保證在執行某個任務時,在它前面進入隊列的所有任務肯定執行完了。對於每一個不同的串行隊列,系統會為這個隊列建立唯一的線程來執行代碼。

      與之相對的是並發隊列:

      並發隊列:比如使用dispatch_get_global_queue。這個隊列中的任務也是按照先來後到的順序開始執行,注意是開始,但是它們的執行結束時間是不確定的,取決於每個任務的耗時。對於n個並發隊列,GCD不會創建對應的n個線程而是進行適當的優化

      我們把整個dispatch_sync看作是一個任務,比如說是非常關鍵、需要高度集中注意力的運鈔過程。這個過程非常重要,一旦開始執行就必須一氣呵成,任何事情都不能干擾這個過程(阻塞線程)。

      現在主線程開始執行這個運鈔任務,任務執行到一半時,突然運鈔員說我好累啊,辛苦了好久了,我現在需要休息(向主線程添加了block)。運鈔員天真的認為,我知道運鈔這個事很重要,本來應該等到運鈔結束後再休息(這樣是串行)。但是在這之前,我的身體條件不允許工作。

      但是之前已經說了,運鈔這件事很重要,它一旦開始就不能結束(阻塞線程)。怎麼能允許有人中途休息呢,因此要休息可以(block是可以執行的),先把鈔票運到安全地方再休息。

      對應到代碼裡面來,當我們想要同步執行這個block的時候,其實是告訴主線程,你把事情處理完了,就過來處理我這個blcok,在此之前我一直等你。而主線程呢,剛處理dispatch_sync函數到一半呢,這個函數還沒返回,哪裡有空去執行block。因此這段代碼運行後,並非卡在block中無法返回,而是根本無法執行到這個block。

      好了,總結一下,到底什麼是死鎖。首先,雖然剛剛我們提到了隊列和線程,以及它們之間的對應關系,但是死鎖一定是針對線程而言的,隊列只是GCD給出的抽象數據結構。所謂的死鎖,一定是發生在一個或多個線程之間的。那麼死鎖和線程阻塞的關系呢,可以這麼理解,雙向的阻塞導致了死鎖。因為阻塞是線程中經常發生的事情,最多就是主線程的阻塞影響了用戶體驗。而一旦出現了雙向的阻塞,就導致了死鎖。我們可以看到,主線程是串行的,在執行某一個任務的時候線程被阻塞了,而這個任務(dispatch_sync)在執行時,又要求阻塞主線程,從而導致了互相的阻塞,也就是死鎖。

      接下來我們思考一下,什麼情況下會導致死鎖。這個問題可能一下子難以得出准確的回答,為了解決這個問題,我打算使用排除法。即先看看什麼情況下不會發生死鎖。比如說,異步執行block肯定不會發生死鎖。比如剛剛的代碼改成這樣:

      dispatch_async(dispatch_get_global_queue(0,0),^(void){
      NSLog(@"這就不死鎖了");
      });

      甚至可以總結出來:異步執行一定不會導致死鎖。因為回顧一下之前導致的死鎖的原因,很重要的一點是主線程在執行dispatch_sync,這是個同步方法,block執行完之前都不會返回。而既然是異步的執行,那麼是立刻返回的,因此不會阻塞主線程。雙向的阻塞不成立了,只是主線程處理blcok時阻塞,但這不會引起死鎖。

      根據之前我們的分析和總結,GCD中我們需要關心的就是同步還是異步執行,以及把block添加到哪個隊列中(串行還是並發)。

      所以接下來就只需要重點思考一下,在同步執行時,什麼時候會導致死鎖。可以再得出一個結論,向並發隊列中添加block不會導致死鎖。再次回顧一下之前導致的死鎖的原因,由於在串行隊列中添加了block,block一直等到前面的任務處理完才會執行,從而導致了死鎖。現在即使是同步的向並發隊列中添加block,GCD會自動為我們管理線程,主線程目前阻塞著(處理這個同步方法),那就新建一個新的線程,但無論如何這個被添加block遲早都會被執行。而所有添加的block被執行完後,同步方法也就返回了。因此不會導致死鎖。

      最後再來討論一下用同步方法向串行隊列添加block的情況,這種情況下會不會造成死鎖呢,答案是不一定。事實上,導致死鎖的原因一定是:

      在某一個串行隊列中,同步的向這個隊列添加block。

      比如文章開頭的例子就屬於這種情況。如果同步的向另外一個串行隊列添加方法,並不一定導致死鎖。比如:

      dispatch_queue_tqueue=dispatch_queue_create("serial",nil);
      dispatch_sync(queue,^(void){
      NSLog(@"這個也不會死鎖");
      });

      分析一下代碼,向名為serial的串行隊列添加任務後,GCD自動創建了一個新的線程,在這個線程中執行block方法。在這個過程中,主線程和新的線程都是阻塞的,但是並不會導致死鎖。

      為什麼說向另一個串行隊列添加任務不一定導致死鎖呢,因為隊列是可以嵌套的,比如在A隊列(串行)添加一個任務a,在a這個任務中向B隊列(串行)添加任務b,在b這個任務中又向A隊列添加任務,這就間接滿足了“在某一個串行隊列中,同步的向這個隊列添加block”。但是我們好像每一次都沒有直接向相同的隊列中添加block。

      所以判斷是否發生死鎖的最好方法就是看有沒有在串行隊列(當然也包括主隊列)中向這個隊列添加任務。又因為我們知道每個串行隊列對應一個線程,所以只要不在某個線程中調用會阻塞這個線程的方法即可。

      事實上,我們使用同步的方法編程,往往是要求保證任務之間的執行順序是完全確定的。且不說GCD提供了很多強大的功能來滿足這個需求,向串行隊列中同步的添加任務本身就是不合理的,畢竟隊列已經是串行的了,直接異步添加就可以了啊。所以,解決文章開頭那個死鎖例子的最簡單的方法就是在合適的位置添加一個字母a。

 
  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved