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

來源:互聯網
上載者:User

iOS陸哥開發筆記(八) (GCD死結及解決方案)
GCD導致死結的原因和解決方案所謂死結,通常指有兩個線程A和B都卡住了,並等待對方完成某些操作。A不能完成是因為它在等待B完成。但B也不能完成,因為它在等待A完成。於是大家都完不成,就導致了死結(DeadLock)。

 

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

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



案例一:Objective-C 
123456 NSLog(@"1");// 任務1dispatch_sync(dispatch_get_main_queue(),^{NSLog(@"2");// 任務2});NSLog(@"3");// 任務3 

結果,控制台輸出:

Objective-C 
12

分析:

  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 
    123456 NSLog(@"1");// 任務1dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0),^{NSLog(@"2");// 任務2});NSLog(@"3");// 任務3 

    結果,控制台輸出:

      Objective-C 
    1234 123 

    分析:

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

    案例三:

     

    Objective-C 
    1234567891011 dispatch_queue_tqueue=dispatch_queue_create("com.demo.serialQueue",DISPATCH_QUEUE_SERIAL);NSLog(@"1");// 任務1dispatch_async(queue,^{NSLog(@"2");// 任務2dispatch_sync(queue,^{NSLog(@"3");// 任務3});NSLog(@"4");// 任務4});NSLog(@"5");// 任務5 

    結果,控制台輸出:

      Objective-C 
    12345 152// 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 
      12345678910 NSLog(@"1");// 任務1dispatch_async(dispatch_get_global_queue(0,0),^{NSLog(@"2");// 任務2dispatch_sync(dispatch_get_main_queue(),^{NSLog(@"3");// 任務3});NSLog(@"4");// 任務4});NSLog(@"5");// 任務5 

      結果,控制台輸出: 

      Objective-C 
      1234567 12534// 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 
      123456789101112 dispatch_async(dispatch_get_global_queue(0,0),^{NSLog(@"1");// 任務1dispatch_sync(dispatch_get_main_queue(),^{NSLog(@"2");// 任務2});NSLog(@"3");// 任務3});NSLog(@"4");// 任務4while(1){}NSLog(@"5");// 任務5 
      Objective-C 
      1 結果,控制台輸出:
      Objective-C 
      1234 14// 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。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.