GCD 容易讓人迷惑的幾個小問題

發表於2016-12-22
112702646-4d2087abfe541c3a

寫在開頭:
本文旨在闡述一些大家容易產生迷惑的GCD相關內容,如果是需要了解一些GCD概念或者基礎用法,可以看看這兩篇文章:GCD 掃盲篇巧談GCD
目錄:
迷惑一:佇列和執行緒的關係
迷惑二:GCD的死鎖
迷惑三:以下這些API的異同與作用場景:
dispatch_asyncdispatch_syncdispatch_barrier_asyncdispatch_barrier_sync

迷惑一:佇列和執行緒的關係
錯誤理解:
  • 有些人會產生一種錯覺,覺得佇列就是執行緒。又有些人會有另外一種錯覺,一個追加Block就是一個執行緒。
正確理解:

對我們使用者來說,與其說GCD是面向執行緒的,不如說是面向佇列的。 它隱藏了內部執行緒的排程。

  • 我們所做的僅僅是建立不同的佇列,把Block追加到佇列中去執行,而佇列是FIFO(先進先出)的。
  • 它會按照我們追加的Block的順序,在綜合我們呼叫的gcd的api(sync、async、dispatch_barrier_async等等),以及根據系統負載來增減執行緒併發數, 來排程執行緒執行Block。
我們來看看以下幾個例子:
例一:我們在主執行緒中,往一個並行queue,以sync的方式提交了一個Block,結果Block在主執行緒中執行。

輸出結果:{number = 1, name = main}

例二:我們在主執行緒中用aync方式提交一個Block,結果Block在分執行緒中執行。

輸出結果:{number = 2, name = (null)}

例三:我們分別用sync和async向主佇列提交Block,結果Block都是在主執行緒中執行:

注意:我們不能直接在主執行緒用sync如下的形式去提交Block,否則會引起死鎖:

我們用sync如下的方式去提交Block:

輸出結果:{number = 1, name = main}

輸出結果:{number = 1, name = main}

總結一下:
  • 往主佇列提交Block,無論是sync,還是async,都是在主執行緒中執行。
  • 往非主佇列中提交,如果是sync,會在當前提交Block的執行緒中執行。如果是async,則會在分執行緒中執行。

上文需要注意以下兩點:

  1. 這裡的syncasync並不侷限於dispatch_syncdispatch_async而指的是GCD中所有同步非同步的API。
  2. 這裡我們用的是並行queue,如果用序列queue,結果也是一樣的。唯一的區別是並行queue會權衡當前系統負載,去同時併發幾條執行緒去執行Block,而序列queue中,始終只會在同一條執行緒中執行Block。
迷惑二:GCD的死鎖

因為很多人因為不理解發生死鎖的原因,所以導致從不會去用sync相關的API,而sync的應用場景還是非常多的,我們不能因噎廢食,所以我們瞭解死鎖原理還是很重要的。

簡單舉個死鎖例子:

首先造成死鎖的原因很簡單,兩個任務間互相等待。
為了加深大家的理解,在這裡我們儘可能用最詳盡,同時也有點繞的方式總結下這個死鎖的流程:

  • 如上,在主執行緒中,往主佇列同步提交了任務一。因為往queue中提交Block,總是追加在佇列尾部的,而queue執行Block的順序為先進先出(FIFO),所以任務一需要在當前佇列它之前的任務(任務二)全部執行完,才能輪到它。
  • 而任務二因為任務一的sync,被阻塞了,它需要等任務一執行完才能被執行。兩者互相等待對方執行完,才能執行,程式被死鎖在這了。
  • 這裡需要注意這裡死鎖的很重要一個條件也因為主佇列是一個序列的佇列(主佇列中只有一條主執行緒)。如果我們如下例,在並行佇列中提交,則不會造成死鎖:

    原因是並行佇列中任務一雖被提交仍然是在queue的隊尾,在任務二之後,但是因為是並行的,所以任務一併不會一直等任務二結束才去執行,而是直接執行完。此時任務二的因為任務一的結束,sync阻塞也就消除,任務二得以執行。

上述第一個死鎖的例子,我們很簡單的改寫一下,死鎖就被消除了:

我們在主執行緒中,往全域性佇列同步提交了Block,因為全域性佇列和主佇列是兩個佇列,所以任務一的執行,並不需要等待任務二。所以等任務一結束,任務二也可以被執行。
當然這裡因為提交Block所在佇列,Block被執行的佇列是完全不同的兩個佇列,所以這裡用序列queue,也是不會死鎖的。大家可以自己寫個例子試試,這裡就不贅述了。

看到這我們可以知道一些sync的阻塞機制:

  • sync提交Block,首先是阻塞的當前提交Block的執行緒(簡單理解下就是阻塞sync之後的程式碼)。例如我們之前舉的例子中,sync總是阻塞了任務二的執行。
  • 而在佇列中,輪到sync提交的Block,僅僅阻塞序列queue,而不會阻塞並行queue。dispatch_barrier_(a)sync除外,我們後面會講到。)

我們瞭解了sync的阻塞機制,再結合發生死鎖的根本原因來自於互相等待,我們用下面一句話來總結一下,會引起GCD死鎖的行為:
如果同步(sync)提交一個Block到一個序列佇列,而提交Block這個動作所處的執行緒,也是在當前佇列,就會引起死鎖。

我相信,如果看明白了上述所說的,基本上可以放心的使用sync相關api,而不用去擔心死鎖的問題。
關於更多死鎖例子這裡就不寫了,基本上都是基於上述所說的,只是在不同佇列中,sync,async組合形式不同,但是原理都是和上述一樣。如果實在感興趣的,可以看看這篇文章:一篇專題讓你秒懂GCD死鎖問題!

迷惑三:以下4個GCD方法的區別:

1)dispatch_async 這個就不用說了,估計大家都用的非常熟悉。
2)dispatch_barrier_async, 這個想必大家也知道是幹嘛用的,如果不知道,我也大概講講:
它的作用可以用一個詞概括--承上啟下,它保證此前的任務都先於自己執行,此後的任務也遲於自己執行。當然它的作用導致它只有在並行佇列中有意義。

例如上述任務,任務1,2,3的順序不一定,4在中間,最後是5,6任務順序不一定。它就像一個柵欄一樣,擋在了一個並行佇列中間。
當然這裡有一點需要注意的是:dispatch_barrier_(a)sync只在自己建立的併發佇列上有效,在全域性(Global)併發佇列、序列佇列上,效果跟dispatch_(a)sync效果一樣。

我們講到這,順便來講講它的用途,例如我們在一個讀寫操作中:
我們要知道一個資料,讀與讀之間是可以用執行緒並行的,但是寫與寫、寫與讀之間,就必須序列同步或者使用執行緒鎖來保證執行緒安全。但是我們有了dispatch_barrier_async,我們就可以如下使用:

這樣寫操作的時候,始終只有它這一條執行緒在進行。而讀操作一直是並行的。這麼做充分利用了多執行緒的優勢,還不需要加鎖,減少了相當一部分的效能開銷。實現了讀寫操作的執行緒安全。

3)dispatch_barrier_sync這個方法和dispatch_barrier_async作用幾乎一樣,都可以在並行queue中當做柵欄。
唯一的區別就是:dispatch_barrier_sync有GCD的sync共有特性,會阻塞提交Block的當前執行緒,而dispatch_barrier_async是非同步提交,不會阻塞。

4)dispatch_sync,我們來講講它和dispatch_barrier_sync的區別。二者因為是sync提交,所以都是阻塞當前提交Block執行緒。
而它倆唯一的區別是:dispatch_sync並不能阻塞並行佇列。其實之前死鎖有提及過,擔心大家感覺疑惑,還是寫個例子:

輸出結果 :
任務三
任務二
任務一

很顯然,並行佇列沒有被sync所阻塞。

dispatch_barrier_sync可以阻塞並行佇列(柵欄作用的體現):

輸出結果 :
任務一
任務二
任務三

總結一下:
這些API都是有各自應用場景的,蘋果也不會給我們提供重複而且毫無意義的方法。
其中在AF的圖片快取處理中,就有大量組合的用到:
dispatch_barrier_syncdispatch_barrier_asyncdispatch_sync
這些API,主要是為了保證在不使用鎖下,快取資料的讀寫的執行緒安全。感興趣的可以去樓主之前的文章中看看:
AFNetworking之UIKit擴充套件與快取實現

大概就寫到這裡了,如果小夥伴有其它感到迷惑的問題,可以評論,樓主會一一回復,如果這個問題問的多的話,會繼續補充在本文中。

相關文章