小笨狼漫談多執行緒:GCD(1)

小笨狼發表於2016-03-02

多執行緒是程式開發中非常基礎的一個概念,大家在開發過程中應該或多或少用過相關的東西。同時這恰恰又是一個比較棘手的概念,一切跟多執行緒掛鉤的東西都會變得複雜。如果使用過程中對多執行緒不夠熟悉,很可能會埋下一些難以預料的坑。

iOS中的多執行緒技術主要有NSThread, GCD和NSOperation。他們的封裝層次依次遞增,其中

  • NSThread封裝性最差,最偏向於底層,主要基於thread使用
  • GCD是基於C的API,直接使用比較方便,主要基於task使用
  • NSOperation是基於GCD封裝的NSObject物件,對於複雜的多執行緒專案使用比較方便,主要基於佇列使用

上篇文章介紹了NSThread的用法,NSThread已經屬於古董級別的東西了,欣賞一下可以,真正使用就不要麻煩他了。GCD是多執行緒中的新貴,比起NSThread更加強大,也更容易使用。由於GCD的東西比較多,我會分好幾篇文章介紹,這篇文章主要介紹GCD中的queue相關知識。

dispatch_queue_t

使用GCD之後,你可以不用再浪費精力去關注執行緒,GCD會幫你管理好一切。你只需要想清楚任務的執行方法(同步還是非同步)和佇列的執行方式(序列還是並行)即可。

任務是一個比較抽象的概念,表示一段用來執行的程式碼,他對應到程式碼裡就是一個block或者一個函式。

佇列分為序列佇列和並行佇列:

  • 序列佇列一次只能執行一個任務。只有一個任務執行完成之後,下一個任務才能執行,主執行緒就是一個序列的佇列。
  • 並行佇列可以同時執行多個任務,系統會維護一個執行緒池來保證並行佇列的執行。執行緒池會根據當前任務量自行安排執行緒的數量,以確保任務儘快執行。

佇列對應到程式碼裡是一個dispatch_queue_t物件:

物件就有記憶體。跟普通OC物件類似,我們可以用dispatch_retain()dispatch_release()對其進行記憶體管理,當一個任務加入到一個queue中的時候,任務會retain這個queue,直到任務執行完成才會release

值得高興的是,iOS6之後,dispatch物件已經支援ARC,所以在ARC工程之下,我們可以不用擔心他的記憶體,想怎麼玩就怎麼玩。

要申明一個dispatch的屬性。一般情況下我們只需要用strong即可。

如果你是寫一個framework,framework的使用者的SDK有可能還是古董級的iOS6之前。那麼你需要根據OS_OBJECT_USE_OBJC做一個判斷是使用strong還是assign。(一般github上的優秀第三方庫都會這麼做)

async

GCD中有2個非同步的API

他們都是將一個任務提交到queue中,提交之後立即返回,不等待任務的的執行。提交之後,系統會對queue做retain操作,任務執行完成之後,queue再被release。兩個函式實際的功能是一樣的,唯一的區別在於dispatch_async接受block作為引數,dispatch_async_f接受函式。

使用dispatch_async的時候block會被copy,在block執行完成之後block再release,由於是系統持有block,所以不用擔心迴圈引用的問題,block裡面的self不需要weak

dispatch_async_f中,context會作為第一個引數傳給work函式。如果work不需要引數,context可以傳入NULL。work引數不能傳入NULL,否則可能發生無法預料的事兒

非同步是一個比較抽象的概念,簡單的說就是將任務加入到佇列中之後,立即返回,不需要等待任務的執行。語言的描述比較抽象,我們用程式碼加深一下對概念的理解

上面這段程式碼,會以這樣的方式執行,紅色表示正在執行的模組,灰色表示未執行或者已經執行完成的模組。

  1. 先在main queue中執行第一個nslog
  2. dispatch_async會將block提交到globalQueue中,提交成功之後立即返回
  3. main queue執行第二個nslog
  4. 等global queue中block前面的任務執行完成之後,block被執行。

sync

與非同步相似,GCD中同步的API也是2個

2個API作用相同:將任務提交到queue中,任務加入queue之後不會立即返回,等待任務執行完成之後再返回。同sync類似,dispatch_syncdispatch_sync_f唯一的區別在於dispatch_sync接收block作為引數,block被系統持有,不需要對self使用weak。dispatch_sync_f接受函式work作為引數,context作為傳給work函式的第一個引數。同樣,work引數也不能傳入NULL,否則會發生無法預料的事兒

同步表示任務加入到佇列中之後不會立即返回,等待任務完成再返回。語言的描述比較抽象,我們再次用程式碼加深一下對概念的理解

我們來看看程式碼的執行方式:

  1. 先在main queue中執行第一個nslog
  2. dispatch_sync會將block提交到global queue中,等待block的執行
  3. global queue中block前面的任務執行完成之後,block執行
  4. block執行完成之後,dispatch_sync返回
  5. dispatch_sync之後的程式碼執行

由於dispatch_sync需要等待block被執行,這就非常容易發生死鎖。如果一個序列佇列,使用dispatch_sync提交block到自己佇列中,就會發生死鎖

dispatch_sync的程式碼執行如圖所示

dispatch_sync需要等待block執行完成,同時由於佇列序列,block的執行需要等待前面的任務,也就是dispatch_sync執行完成。兩者互相等待,永遠也不會執行完成,死鎖就這樣發生了

從這裡看發生死鎖需要2個條件:

  1. 程式碼執行的當前佇列是序列佇列
  2. 使用sync將任務加入到自己佇列中

如果queue是並行佇列,或者將任務加入到其他佇列中,這是不會發生死鎖的。

獲取佇列

獲取主執行緒佇列

主執行緒是我們最常用的執行緒,GCD提供了非常簡單的獲取主執行緒佇列的方法。

方法不需要傳入引數,直接返回主執行緒佇列。
假設我們要在主執行緒更新UI:

執行加入到主執行緒佇列的block,App會呼叫dispatch_main(), NSApplicationMain(),或者在主執行緒使用CFRunLoop

獲取全域性佇列

除了主執行緒佇列,GCD提供了幾個全域性佇列,可以直接獲取使用

dispatch_get_global_queue方法獲取的全域性佇列都是並行佇列,並且佇列不能被修改,也就是說對全域性佇列呼叫dispatch_suspend(), dispatch_resume(), dispatch_set_context()等方法無效

  • identifier: 用以標識佇列優先順序,推薦用qos_class列舉作為引數,也可以使用dispatch_queue_priority_t
  • flags: 預留欄位,傳入任何非0的值都可能導致返回NULL

可以看到dispatch_get_global_queue根據identifier引數返回相應的全域性佇列。identifier推薦使用qos_class列舉

這個列舉與NSThread中的NSQualityOfService類似

  • QOS_CLASS_USER_INTERACTIVE: 最高優先順序,互動級別。使用這個優先順序會佔用幾乎所有的系統CUP和I/O頻寬,僅限用於互動的UI操作,比如處理點選事件,繪製影像到螢幕上,動畫等
  • QOS_CLASS_USER_INITIATED: 次高優先順序,用於執行類似初始化等需要立即返回的事件
  • QOS_CLASS_DEFAULT: 預設優先順序,當沒有設定優先順序的時候,執行緒預設優先順序。一般情況下用的都是這個優先順序
  • QOS_CLASS_UTILITY: 普通優先順序,主要用於不需要立即返回的任務
  • QOS_CLASS_BACKGROUND: 後臺優先順序,用於使用者幾乎不感知的任務。
  • QOS_CLASS_UNSPECIFIED: 未知優先順序,表示服務質量資訊缺失

identifier除了使用qos_class列舉,也可以用dispatch_queue_priority_t作為引數。

dispatch_queue_priority_t對應到qos_class列舉有:

很多時候我們喜歡將0或者NULL傳入作為引數

由於NULL等於0,也就是DISPATCH_QUEUE_PRIORITY_DEFAULT,所以返回的是預設優先順序

建立佇列

當無法獲取到理想的佇列時,我們可以自己建立佇列。

如果未使用ARC,dispatch_queue_create建立的queue在使用結束之後需要呼叫dispatch_release

  • label: 佇列的名稱,除錯的時候可以區分其他的佇列
  • attr: 佇列的屬性,dispatch_queue_attr_t型別。用以標識佇列序列,並行,以及優先順序等資訊

attr引數有三種傳值方式:

DISPATCH_QUEUE_SERIAL或者NULL,表示建立序列佇列,優先順序為目標佇列優先順序。DISPATCH_QUEUE_CONCURRENT表示建立並行佇列,優先順序也為目標佇列優先順序。

dispatch_queue_attr_make_with_qos_class函式可以建立帶有優先順序的dispatch_queue_attr_t物件。通過這個物件可以自定義queue的優先順序。

  • attr: 傳入DISPATCH_QUEUE_SERIALNULL或者DISPATCH_QUEUE_CONCURRENT,表示序列或者並行
  • qos_class: 傳入qos_class列舉,表示優先順序級別
  • relative_priority: 相對於qos_class的相對優先順序,qos_class用於區分大的優先順序級別,relative_priority表示大級別下的小級別。relative_priority必須大於QOS_MIN_RELATIVE_PRIORITY小於0,否則將返回NULL。從GCD原始碼中可以查到QOS_MIN_RELATIVE_PRIORITY等於-15

使用dispatch_queue_attr_make_with_qos_class建立佇列時,需要注意,非法的引數可能導致dispatch_queue_attr_make_with_qos_class返回NULL,dispatch_queue_create傳入NULL會建立出序列佇列。寫程式碼過程中需要確保這是否是預期的結果

設定目標佇列(2.25日更新,感謝@楊蕭玉HIT 指出問題,原文章有誤給大家致歉)

除了通過dispatch_queue_attr_make_with_qos_class設定佇列的優先順序之外,也可以使用設定目標佇列的方法,設定佇列的優先順序。當佇列建立時未設定優先順序,佇列將繼承目標佇列的優先順序。(不過一般情況下還是推薦使用dispatch_queue_attr_make_with_qos_class設定佇列的優先順序)

呼叫dispatch_set_target_queue會retain新目標佇列queue,release原有目標佇列。設定目標佇列之後,block將會在目標佇列中執行。注意:當目標佇列序列時,任何在目標佇列中執行的block都會序列執行,無論原佇列是否序列

假設有佇列A、B是並行佇列,C為序列佇列。A,B的目標佇列均設定為C,那麼A、B、C中的block在設定目標佇列之後最終都會序列執行

例:
佇列1並行,佇列2序列

執行一下可知block1,block2,block3並行執行

如果將佇列1的目標佇列設定為佇列2,會發生什麼情況呢?

block1,block2,block3變為了序列

注意不要迴圈設定目標佇列,如A的目標佇列為B,B的目標佇列為A。這將會導致無法預知的錯誤

延時

GCD中有2個延時的API

一定時間之後將block加入到queue中。when用於表示時間,如果傳入DISPATCH_TIME_NOW會等同於dispatch_async。另外不允許傳入DISPATCH_TIME_FOREVER,這會永遠阻塞執行緒。

通前面其他方法類似。dispatch_after接收block作為引數,系統持有block,block中self不需要weak。dispatch_after_f接收work函式作為引數,context作為work函式的第一個引數

需要注意的是這裡的延時是不精確的,因為加入佇列不一定會立即執行。延時1s可能會1.5s甚至2s之後才會執行。

dispatch_barrier

在並行佇列中,有的時候我們需要讓某個任務單獨執行,也就是他執行的時候不允許其他任務執行。這時候dispatch_barrier就派上了用場。

使用dispatch_barrier將任務加入到並行佇列之後,任務會在前面任務全部執行完成之後執行,任務執行過程中,其他任務無法執行,直到barrier任務執行完成

dispatch_barrier在GCD中有4個API

如果API在序列佇列中呼叫,將等同於dispatch_asyncdispatch_async_fdispatch_syncdispatch_sync_f,不會有任何影響。

dispatch_barrier最典型的使用場景是讀寫問題,NSMutableDictionary在多個執行緒中如果同時寫入,或者一個執行緒寫入一個執行緒讀取,會發生無法預料的錯誤。但是他可以在多個執行緒中同時讀取。如果多個執行緒同時使用同一個NSMutableDictionary。怎樣才能保護NSMutableDictionary不發生意外呢?

當NSMutableDictionary寫入的時候,我們使用dispatch_barrier_async,讓其單獨執行寫入操作,不允許其他寫入操作或者讀取操作同時執行。當讀取的時候,我們只需要直接使用dispatch_sync,讓其正常讀取即可。這樣就可以保證寫入時不被打擾,讀取時可以多個執行緒同時進行

set_specific & get_specific

有時候我們需要將某些東西關聯到佇列上,比如我們想在某個佇列上存一個東西,或者我們想區分2個佇列。GCD提供了dispatch_queue_set_specific方法,通過key,將context關聯到queue上

  • queue:需要關聯的queue,不允許傳入NULL
  • key:唯一的關鍵字
  • context:要關聯的內容,可以為NULL
  • destructor:釋放context的函式,當新的context被設定時,destructor會被呼叫

有存就有取,將context關聯到queue上之後,可以通過dispatch_queue_get_specific或者dispatch_get_specific方法將值取出來。

  • dispatch_queue_get_specific: 根據queue和key取出context,queue引數不能傳入全域性佇列
  • dispatch_get_specific: 根據唯一的key取出當前queue的context。如果當前queue沒有key對應的context,則去queue的target queue取,取不著返回NULL,如果對全域性佇列取,也會返回NULL

iOS6之後dispatch_get_current_queue()被廢棄(廢棄的原因這裡不多解釋,如果想了解可以看這裡),如果我們需要區分不同的queue,可以使用set_specific方法。根據對應的key是否有值來區分

END

節後第一彈,queue相關的內容就介紹到這裡,GCD的東西挺多,其他東西之後如果有時間我會慢慢介紹,敬請期待

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

小笨狼漫談多執行緒:GCD(1)

相關文章