GCD詳解

1、GCD簡介

  • Grand Central Dispatch(GCD) 是 Apple 開發(fā)的一個(gè)多核編程的較新的解決方法。它主要用于優(yōu)化應(yīng)用程序以支持多核處理器以及其他對稱多處理系統(tǒng)。它是一個(gè)在線程池模式的基礎(chǔ)上執(zhí)行的并發(fā)任務(wù)。

  • GCD是一個(gè)替代諸如NSThread等技術(shù)的很高效和強(qiáng)大的技術(shù)。GCD完全可以處理諸如數(shù)據(jù)鎖定和資源泄漏等復(fù)雜的異步編程問題。GCD的工作原理是讓一個(gè)程序,根據(jù)可用的處理資源,安排他們在任何可用的處理器核心上平行排隊(duì)執(zhí)行特定的任務(wù)。這個(gè)任務(wù)可以是一個(gè)功能或者一個(gè)程序段。

GCD相關(guān)的一些基本概念.png

GCD整體結(jié)構(gòu)

2、GCD優(yōu)點(diǎn):

  • 可用于多核的并行運(yùn)算

  • 會自動(dòng)利用更多的 CPU 內(nèi)核(比如雙核、四核)

  • 會自動(dòng)管理線程的生命周期(創(chuàng)建線程、調(diào)度任務(wù)、銷毀線程)

  • 程序員只需要告訴 GCD 想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼

3、GCD 任務(wù)和隊(duì)列

  • 任務(wù):執(zhí)行操作的意思,換句話說就是你在線程中執(zhí)行的那段代碼。在 GCD 中是放在 block 中的。執(zhí)行任務(wù)有兩種方式:同步執(zhí)行(sync)和異步執(zhí)行(async)。

    • 同步執(zhí)行(sync):同步添加任務(wù)到指定的隊(duì)列中,在添加的任務(wù)執(zhí)行結(jié)束之前,會一直等待,直到隊(duì)列里面的任務(wù)完成之后再繼續(xù)執(zhí)行。只能在當(dāng)前線程中執(zhí)行任務(wù),不具備開啟新線程的能力。

    • 異步執(zhí)行(async):異步添加任務(wù)到指定的隊(duì)列中,它不會做任何等待,可以繼續(xù)執(zhí)行任務(wù)??梢栽谛碌木€程中執(zhí)行任務(wù),具備開啟新線程的能力。

    • 備注:異步執(zhí)行(async)雖然具有開啟新線程的能力,但是并不一定開啟新線程。這跟任務(wù)所指定的隊(duì)列類型有關(guān)。

    • 兩者的主要區(qū)別是:是否等待隊(duì)列的任務(wù)執(zhí)行結(jié)束,以及是否具備開啟新線程的能力。

  • 隊(duì)列:這里的隊(duì)列指執(zhí)行任務(wù)的等待隊(duì)列,即用來存放任務(wù)的隊(duì)列。隊(duì)列是一種特殊的線性表,采用 FIFO(先進(jìn)先出)的原則,即新任務(wù)總是被插入到隊(duì)列的末尾,而讀取任務(wù)的時(shí)候總是從隊(duì)列的頭部開始讀取。每讀取一個(gè)任務(wù),則從隊(duì)列中釋放一個(gè)任務(wù)。隊(duì)列的結(jié)構(gòu)可參考下圖:

    • 串行隊(duì)列(Serial Dispatch Queue):每次只有一個(gè)任務(wù)被執(zhí)行。讓任務(wù)一個(gè)接著一個(gè)地執(zhí)行。(只開啟一個(gè)線程,一個(gè)任務(wù)執(zhí)行完畢后,再執(zhí)行下一個(gè)任務(wù))

    • 并發(fā)隊(duì)列(Concurrent Dispatch Queue):可以讓多個(gè)任務(wù)并發(fā)(同時(shí))執(zhí)行,可以開啟多個(gè)線程,并且同時(shí)執(zhí)行任務(wù)

      • 備注:并發(fā)隊(duì)列的并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效

關(guān)于同步異步、串行并行和線程的關(guān)系,下面通過一個(gè)表格來總結(jié):

4、GCD 的使用步驟

  • 創(chuàng)建一個(gè)隊(duì)列(串行隊(duì)列或并發(fā)隊(duì)列)

  • 將任務(wù)追加到任務(wù)的等待隊(duì)列中,然后系統(tǒng)就會根據(jù)任務(wù)類型執(zhí)行任務(wù)(同步執(zhí)行或異步執(zhí)行)

4.1、隊(duì)列的創(chuàng)建方法/獲取方法

可以使用dispatch_queue_create來創(chuàng)建隊(duì)列,需要傳入兩個(gè)參數(shù),第一個(gè)參數(shù)表示隊(duì)列的唯一標(biāo)識符,用于 DEBUG,可為空,Dispatch Queue 的名稱推薦使用應(yīng)用程序 ID 這種逆序全程域名;第二個(gè)參數(shù)用來識別是串行隊(duì)列還是并發(fā)隊(duì)列。

  • DISPATCH_QUEUE_SERIAL 表示串行隊(duì)列
  • DISPATCH_QUEUE_CONCURRENT 表示并發(fā)隊(duì)列
  • 對于串行隊(duì)列,GCD 提供了的一種特殊的串行隊(duì)列:主隊(duì)列(Main Dispatch Queue)。
    所有放在主隊(duì)列中的任務(wù),都會放到主線程中執(zhí)行。

    可使用dispatch_get_main_queue()獲得主隊(duì)列。

  • 對于并發(fā)隊(duì)列,GCD 默認(rèn)提供了全局并發(fā)隊(duì)列(Global Dispatch Queue)

    可以使用dispatch_get_global_queue來獲取。需要傳入兩個(gè)參數(shù)。第一個(gè)參數(shù)表示隊(duì)列優(yōu)先級,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二個(gè)參數(shù)暫時(shí)沒用,用0即可。

4.2、 任務(wù)的創(chuàng)建方法

GCD 提供了同步執(zhí)行任務(wù)的創(chuàng)建方法dispatch_sync和異步執(zhí)行任務(wù)創(chuàng)建方法dispatch_async。

不同組合方式

5、 柵欄方法(dispatch_barrier_async和dispatch_barrier_sync)

實(shí)現(xiàn)多讀單寫

輸出的結(jié)果:
2018-09-21 14:36:35.946008+0800 GCDDemo[3665:625310] aa, <NSThread: 0x600002f013c0>{number = 1, name = main}
2018-09-21 14:36:35.946012+0800 GCDDemo[3665:625532] ----1-----<NSThread: 0x600002f57dc0>{number = 3, name = (null)}
2018-09-21 14:36:35.946027+0800 GCDDemo[3665:625531] ----2-----<NSThread: 0x600002f5d3c0>{number = 4, name = (null)}
2018-09-21 14:36:35.946139+0800 GCDDemo[3665:625310] bb, <NSThread: 0x600002f013c0>{number = 1, name = main}
2018-09-21 14:36:35.946161+0800 GCDDemo[3665:625531] ----barrier-----<NSThread: 0x600002f5d3c0>{number = 4, name = (null)}
2018-09-21 14:36:35.946258+0800 GCDDemo[3665:625532] ----4-----<NSThread: 0x600002f57dc0>{number = 3, name = (null)}
2018-09-21 14:36:35.946277+0800 GCDDemo[3665:625531] ----3-----<NSThread: 0x600002f5d3c0>{number = 4, name = (null)}

從打印中可以總結(jié)如下:

  • aa和bb都在主線程進(jìn)行輸出。

  • 先執(zhí)行完barrier之前的任務(wù),然后再執(zhí)行自己的任務(wù)(barrier),最后執(zhí)行barrier之后的任務(wù)(注意這里說的是任務(wù)不是下一行代碼)。

  • 將自己的任務(wù)(barrier)插入到隊(duì)列之后,不會等待自己的任務(wù)結(jié)束,它會繼續(xù)把后面的任務(wù)(3、4)插入到隊(duì)列,然后執(zhí)行任務(wù)。

從打印中可以總結(jié)如下:

  • aa和bb都在主線程進(jìn)行輸出。

  • barrier隊(duì)列里面任務(wù)在主線程中執(zhí)行。

  • 先執(zhí)行完barrier之前的任務(wù),然后再執(zhí)行自己的任務(wù)(barrier),最后執(zhí)行barrier之后的任務(wù)(注意這里說的是任務(wù)不是下一行代碼)。???

  • 需要等待自己的任務(wù)(barrier)結(jié)束之后,才會繼續(xù)添加并執(zhí)行寫在barrier后面的任務(wù)(3、4),然后執(zhí)行后面的任務(wù)。???

  • 注意點(diǎn):在使用柵欄函數(shù)時(shí),需使用自定義隊(duì)列才有意義,如果用的是串行隊(duì)列或者系統(tǒng)提供的全局并發(fā)隊(duì)列,這個(gè)柵欄函數(shù)的作用等同于一個(gè)同步函數(shù)的作用。

不同點(diǎn):

  • dispatch_barrier_async將自己的任務(wù)(barrier)插入到隊(duì)列之后,不會等待自己的任務(wù)結(jié)束,它會繼續(xù)把后面的任務(wù)(3、4)插入到隊(duì)列,然后執(zhí)行任務(wù)。

  • dispatch_barrier_sync需要等待自己的任務(wù)(barrier)結(jié)束之后,才會繼續(xù)添加并執(zhí)行寫在barrier后面的任務(wù)(3、4),然后執(zhí)行后面的任務(wù)。

6、GCD隊(duì)列組(dispatch_group)

  • 1、dispatch_group_notify

  • 2、dispatch_group_wait

  • 3、dispatch_group_enter和dispatch_group_leave

7、GCD信號量(dispatch_semaphore)

GCD 信號量:dispatch_semaphore.png

常見面試題

  • 1、已經(jīng)在主線程了,還同步向主線程扔了一個(gè)任務(wù)

假設(shè)當(dāng)前我們的代碼正在queue0中執(zhí)行。然后我們調(diào)用disptach_sync將一個(gè)任務(wù)block1扔到queue0中執(zhí)行,dispatch_sync(queue0, block1);

這時(shí),dispatch_sync將等待queue0排隊(duì)執(zhí)行完block1,然后才能繼續(xù)執(zhí)行下一行代碼。But,當(dāng)前代碼執(zhí)行的環(huán)境也是queue0。假設(shè)當(dāng)前執(zhí)行的任務(wù)為block0。也就是說,block0在執(zhí)行到一半時(shí),需要等到自己的下一個(gè)任務(wù)block1執(zhí)行完,自己才能繼續(xù)執(zhí)行。而block1排隊(duì)在后面,需要等block0執(zhí)行完才能執(zhí)行。這時(shí)死鎖就產(chǎn)生了,block0和block1互相等待執(zhí)行,當(dāng)前線程就卡死在dispatch_sync這行代碼處。

安全方法

YYKit中提供了一個(gè)同步扔任務(wù)到主線程的安全方法:

其方式就是在扔任務(wù)給主線程之前,先檢查當(dāng)前線程是否已經(jīng)是主線程,如果是,就不用調(diào)用GCD的隊(duì)列調(diào)度接口dispatch_sync了,直接執(zhí)行即可;如果不是主線程,那么調(diào)用GCD的dispatch_sync也不會卡死。

但事實(shí)上并不是這樣的,dispatch_sync_on_main_queue也可能會卡死,這個(gè)安全接口并不安全。這個(gè)接口只能保證兩個(gè)block之間不因互相等待而死鎖。多于兩個(gè)block的互相依賴就束手無策了。

舉個(gè)例子,假設(shè)queue0是一個(gè)子線程的隊(duì)列:

在上述代碼中,block0正在主線程中執(zhí)行,并且同步等待子線程執(zhí)行完block1。block1又同步等待主線程執(zhí)行完block2。而當(dāng)前主線程正在執(zhí)行block0,即block2的執(zhí)行需要等到block0執(zhí)行完。這樣就成了block0-->block1-->block2-->block0...這樣一個(gè)循環(huán)等待,即死鎖。由于block1的環(huán)境是子線程,所以安全API的線程判斷不起任何作用。

由于通知NSNotification的執(zhí)行是同步的,這里會出現(xiàn)和上一例一樣的死鎖情況:block0-->block1-->block2-->block0...

  • 2、輸入順序

串行隊(duì)列+同步執(zhí)行

  • 特點(diǎn):不會開啟新線程,在當(dāng)前線程執(zhí)行任務(wù),并且任務(wù)是串行的,執(zhí)行完一個(gè)任務(wù),再執(zhí)行下一個(gè)任務(wù)。

viewDidLoad是在主隊(duì)列中主線程執(zhí)行的。同步提交一個(gè)任務(wù)到串行隊(duì)列,此時(shí),該任務(wù)將會在主線程執(zhí)行完畢,然后繼續(xù)執(zhí)行主隊(duì)列中的其他操作。

從打印中可以總結(jié)如下幾點(diǎn):

  • 所有任務(wù)都是在主線程中執(zhí)行的,并沒有開啟新的線程(同步執(zhí)行不具備開啟新線程的能力),由于串行隊(duì)列,所以按順序一個(gè)一個(gè)執(zhí)行下去。

  • 所有任務(wù)都在打印的begin-- 和end-- 之間,這說明任務(wù)是添加到隊(duì)列中馬上執(zhí)行的,而且同步任務(wù)需要等待隊(duì)列的任務(wù)執(zhí)行結(jié)束,才可以執(zhí)行下一個(gè)任務(wù)。

  • 任務(wù)是按順序執(zhí)行的,這說明串行隊(duì)列每次只有一個(gè)任務(wù)被執(zhí)行,任務(wù)一個(gè)接一個(gè)按順序執(zhí)行下去。

串行隊(duì)列+異步執(zhí)行

  • 特點(diǎn):會開啟新線程,但是因?yàn)槿蝿?wù)是串行的,所以執(zhí)行完一個(gè)任務(wù)之后,才會再執(zhí)行下一個(gè)任務(wù)。

從打印中可以總結(jié)如下幾點(diǎn):

  • 開啟了一條新線程(異步執(zhí)行具備開啟新線程的能力,但是串行隊(duì)列只能開啟一個(gè)線程),但是任務(wù)由于是串行的,所以任務(wù)還是一個(gè)一個(gè)的執(zhí)行下去(串行隊(duì)列每次只能有一個(gè)任務(wù)被執(zhí)行,任務(wù)一個(gè)接著一個(gè)順序執(zhí)行)。

  • 所有任務(wù)是在打印的begin-- 和end-- 之后才開始執(zhí)行的,說明任務(wù)是將所有任務(wù)添加到隊(duì)列之后才開始同步執(zhí)行,而不是馬上執(zhí)行(異步執(zhí)行不會做任何等待,可以繼續(xù)執(zhí)行任務(wù))。

并發(fā)隊(duì)列+同步執(zhí)行

GCD[2591:161026] 1
GCD[2591:161026] 2
GCD[2591:161026] 3
GCD[2591:161026] 4
GCD[2591:161026] 5

從打印中可以總結(jié)如下幾點(diǎn):

  • 所有任務(wù)都是在當(dāng)前線程(主線程)中執(zhí)行的,沒有開啟新的線程(同步執(zhí)行不具備開啟新線程的能力)。
  • 所有任務(wù)按順序執(zhí)行的。
  • 所有任務(wù)都在打印的begin--和end--之間,由于同步任務(wù)需要等待隊(duì)列的任務(wù)執(zhí)行結(jié)束才能執(zhí)行下一個(gè)任務(wù)。

疑問點(diǎn):并發(fā)隊(duì)列具備開啟多個(gè)線程能力為什么沒不能同時(shí)執(zhí)行任務(wù)呢?

  • 答:雖然并發(fā)隊(duì)列可以開啟多個(gè)線程,并且可以同時(shí)執(zhí)行多個(gè)任務(wù)。但是其本身不能創(chuàng)建新線程,因?yàn)橥饺蝿?wù)不具備開啟新線程的能力,所以只有當(dāng)前線程這一個(gè)線程,所以也就不存在并發(fā)。而且當(dāng)前線程只有等待當(dāng)前隊(duì)列中正在執(zhí)行的任務(wù)執(zhí)行完畢之后,才能繼續(xù)接著執(zhí)行下面的操作(同步任務(wù)需要等待隊(duì)列的任務(wù)執(zhí)行結(jié)束)。所以任務(wù)只能一個(gè)接一個(gè)按順序執(zhí)行,不能同時(shí)被執(zhí)行。

并發(fā)隊(duì)列+異步執(zhí)行

特點(diǎn):可以開啟多個(gè)線程,任務(wù)交替(同時(shí))執(zhí)行。

從打印中可以總結(jié)如下幾點(diǎn):

  • 除了主線程,又開啟了3個(gè)線程,并且任務(wù)是交替著同時(shí)執(zhí)行的,由于異步執(zhí)行具備開啟新線程的能力,且并發(fā)隊(duì)列可開啟多個(gè)線程,同時(shí)執(zhí)行多個(gè)任務(wù).

  • 所有任務(wù)是在打印的begin-- 和end-- 之后才開始執(zhí)行的,說明任務(wù)不是馬上執(zhí)行,而是將所有任務(wù)添加到隊(duì)列之后才開始異步執(zhí)行,另外當(dāng)前線程并沒有等待,而是直接開啟了新的線程,在新線程中執(zhí)行任務(wù),由于異步執(zhí)行不做等待,所以可以繼續(xù)執(zhí)行其他任務(wù).

主隊(duì)列+同步執(zhí)行

  • 特點(diǎn):1.主線程調(diào)用:互等卡主不執(zhí)行。 2.其他線程調(diào)用:不會開啟新線程,執(zhí)行完一個(gè)任務(wù),再執(zhí)行下一個(gè)任務(wù)。

從打印中可以總結(jié)如下:

  • 程序崩潰,只打印出begin-- 主隊(duì)列 + 同步執(zhí)行
  • 疑問點(diǎn):為什么任務(wù)沒有執(zhí)行完,而且程序了崩潰呢?
  • 答:我們在主線程中執(zhí)行syncMain方法,相當(dāng)于把syncMain任務(wù)放到了主線程的隊(duì)列中,主線程正在處理syncMain這個(gè)任務(wù) 而syncMain方法中又有同步事件需要處理 ,造成會相互等待,所以死鎖了,所以任務(wù)不會執(zhí)行完。

其他線程調(diào)用

從打印中可以總結(jié)如下:

  • 所有任務(wù)都是在主線程中執(zhí)行的,并沒有開啟新的線程(所有放在主隊(duì)列中的任務(wù),都會放到主線程中執(zhí)行),而且由于主隊(duì)列是串行隊(duì)列,所以按順序一個(gè)一個(gè)執(zhí)行。

  • 所有任務(wù)都在打印的begin-- 和end-- 之間,這說明任務(wù)是添加到隊(duì)列中馬上執(zhí)行的。

  • 疑問點(diǎn):為什么在其他線程調(diào)用不會崩潰卡住呢?

  • 答:因?yàn)閟yncMain任務(wù)放到了其他線程里,而syncMain方法中的幾個(gè)任務(wù)都追加到主隊(duì)列中,因?yàn)橹麝?duì)列現(xiàn)在沒有正在執(zhí)行的任務(wù),所以會直接執(zhí)行主隊(duì)列的任務(wù),一個(gè)個(gè)執(zhí)行下去,所以這里不會卡住線程。

主隊(duì)列+異步執(zhí)行

  • 特點(diǎn):只在主線程中執(zhí)行任務(wù),執(zhí)行完一個(gè)任務(wù),再執(zhí)行下一個(gè)任務(wù)。

從打印中可以總結(jié)如下:

  • 1.所有任務(wù)都是在主線程中執(zhí)行的,并沒有開啟新的線程,雖然異步執(zhí)行具備開啟線程的能力,但因?yàn)槭侵麝?duì)列,所以所有任務(wù)都在主隊(duì)列(主隊(duì)列是串行隊(duì)列)中,并且一個(gè)接一個(gè)的執(zhí)行下去.

  • 2.所有任務(wù)是在打印的begin-- 和end-- 之后才開始執(zhí)行的,說明任務(wù)并不是馬上執(zhí)行,而是將所有任務(wù)添加到隊(duì)列之后才開始同步執(zhí)行。

PerformSelector

PerformSelector 相關(guān),同步在當(dāng)前線程執(zhí)行的。會阻塞當(dāng)前線程。可在主線程或者子線程執(zhí)行。

PerformSelector:afterDelay 任務(wù)會在默認(rèn)的系統(tǒng)底層的一個(gè)線程上執(zhí)行,而這個(gè)線程是沒有開啟runLoop。需要一個(gè)提交到runloop的邏輯的,所以該方法會失效。

GCD[2672:168690] 4
GCD[2672:168728] 1
GCD[2672:168728] 3

輸出題(自行檢測)

GCD[1874:105153] 1
GCD[1874:105153] 5
GCD[1874:105241] 2

上圖解釋如下:因?yàn)槭钱惒讲l(fā),創(chuàng)建了子線程執(zhí)行任務(wù)。之后由于在主隊(duì)列提交了一個(gè)同步任務(wù),主線程的ViewDidLoad需要等待該同步任務(wù)的執(zhí)行完成,同時(shí)該任務(wù)也需要主線程執(zhí)行完成,產(chǎn)生死鎖。

GCD[1918:108523] 1
GCD[1918:108523] 5
GCD[1918:108602] 2
GCD[1918:108602] 3
GCD[1918:108602] 4

上圖解釋如下:在執(zhí)行完任務(wù)的子線程中,隨后模式是 串行+同步,按順序執(zhí)行,先執(zhí)行2,然后提交在串行隊(duì)列的同步任務(wù),等任務(wù)返回,再執(zhí)行之后的任務(wù)。

GCD[1918:108523] 1
GCD[1918:108523] 5
GCD[1918:108602] 2
GCD[1918:108602] 3
GCD[1918:108602] 4

上圖解釋如下:并發(fā)隊(duì)列+異步執(zhí)行,創(chuàng)建子線程執(zhí)行任務(wù)。在子線程執(zhí)行主隊(duì)列的同步任務(wù)。不會影響主線程的執(zhí)行。

程序崩潰,出現(xiàn)了死鎖。串行隊(duì)列+異步執(zhí)行,會開啟新線程,但是因?yàn)槿蝿?wù)是串行的,所以執(zhí)行完一個(gè)任務(wù)之后,才會再執(zhí)行下一個(gè)任務(wù)。然后在串行隊(duì)列中又有任務(wù),產(chǎn)生死鎖。

dispatch_group_enter和dispatch_group_leave

  • 特點(diǎn):dispatch_group_enterdispatch_group_leave總是成對出現(xiàn)的。
  • dispatch_group_enter:用于添加對應(yīng)任務(wù)組中的未執(zhí)行完畢的任務(wù)數(shù),執(zhí)行一次,未執(zhí)行完畢的任務(wù)數(shù)加1,當(dāng)未執(zhí)行完畢任務(wù)數(shù)為0的時(shí)候,才會使dispatch_group_wait解除阻塞和dispatch_group_notify的block執(zhí)行。
  • dispatch_group_leave:用于減少任務(wù)組中的未執(zhí)行完畢的任務(wù)數(shù),執(zhí)行一次,未執(zhí)行完畢的任務(wù)數(shù)減1,dispatch_group_enter和dispatch_group_leave要匹配,不然系統(tǒng)會認(rèn)為group任務(wù)沒有執(zhí)行完畢。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容