多線程編程(四)GCD

前文中,我們介紹了多線程的基本概念和多線程編程實現的兩種方式,本文,我們介紹一下最後一種多線程編程工具,也是最重要的一種:GCD。


1. GCD簡介

1.1 什麼是GCD

GCD全稱是Grand Central Dispatch,可譯爲“牛逼的中樞調度器” ,純C語言,提供了非常多強大的函數。GCD爲我們編寫代碼提供了非常絕佳的體驗。它是蘋果公司爲在多核心設備上實現多線程充分利用多核優勢提供的解決方案,在很多技術中,如 RunLoop GCD都扮演了重要的角色。GCD全稱Grand Central Dispatch,直譯爲調度中心,完全基於C語言編寫。使用GCD,不需要管理線程,線程管理完全託管給GCD。把線程想象成的火車,我們只需要提交我們的運送任務,GCD會幫我們在合適的火車上運行任務。多線程編程變得如此簡單,但對於初學者來講,GCD學習會有一定難度。因爲是純C編寫。所以沒有對象的概念,學習GCD你可以忘掉封裝繼承多態等等概念。

1.2 GCD的主要優勢

GCD提供很多超越傳統多線程編程的優勢:

  • 易用: GCD比之thread跟簡單易用,程序員只需要告訴GCD想要執行什麼任務,不需要編寫任何線程管理代碼。由於GCD基於work unit而非像thread那樣基於運算,所以GCD可以控制諸如等待任務結束、監視文件描述符、週期執行代碼以及工作掛起等任務。基於block的血統導致它能極爲簡單得在不同代碼作用域之間傳遞上下文。

  • 效率: GCD是蘋果公司爲多核的並行運算提出的解決方案,GCD會自動利用更多的CPU內核(比如雙核、四核)。GCD被實現得如此輕量和優雅,使得它在很多地方比之專門創建消耗資源的線程更實用且快速。這關係到易用性:導致GCD易用的原因有一部分在於你可以不用擔心太多的效率問題而僅僅使用它就行了。

  • 性能: GCD會自動管理線程的生命週期(創建線程、調度任務、銷燬線程)。GCD自動根據系統負載來增減線程數量,這就減少了上下文切換以及增加了計算效率。

1.3 使用GCD需要注意的幾點

  • GCD存在於libdispatch.dylib這個庫中,這個調度庫包含了GCD的所有的東西,但任何IOS程序,默認就加載了這個庫,在程序運行的過程中會動態的加載這個庫,不需要我們手動導入。
    這裏寫圖片描述
    這裏寫圖片描述
  • GCD是純C語言的,因此我們在編寫GCD相關代碼的時候,面對的函數,而不是方法。
  • GCD中的函數大多數都以dispatch開頭。

1.4 GCD中有2個核心概念

  • 隊列:用來存放任務
  • 任務:執行什麼操作

1.5 GCD的使用的2個步驟

(1)定製隊列
(2)提交任務,確定想做的事情

注意:將任務添加到隊列中,GCD會自動將隊列中的任務取出,放到對應的線程中執行
提示:任務的取出遵循隊列的FIFO原則:先進先出,後進後出

2. 創建隊列

GCD中的一個重要組成部分就是隊列,我們把各種任務提交給隊列,隊列根據它本身的類型以及當前系統的狀態,添加到不同的線程中執行任務。線程的創建和管理都有GCD 本身完成,不需要我們參與。系統提供了很多定義好的隊列:只管理主線程的main_queue,全局並行的 globle_queue。同時,我們也可以自定義自己的隊列queue。

GCD的隊列可以分爲2大類型:
1. 併發隊列(Concurrent Dispatch Queue)
   可以讓多個任務併發(同時)執行(自動開啓多個線程同時執行任務)併發功能只有在異步(dispatch_async)函數下才有效
2. 串行隊列(Serial Dispatch Queue)
   讓任務一個接着一個地執行(一個任務執行完畢後,再執行下一個任務)所有線程串行,或者只有一個線程,任務依次執行。

queue的類型爲 dispatch_queue_t。
創建函數是:dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
第一個參數代表queue的名字,注意是char型指針,並非NSString;
第二個參數表示queue的類型,

  • DISPATCH_QUEUE_SERIAL表示串行 queue。
  • DISPATCH_QUEUE_CONCURRENT表 示並行queue。

2.1 串行隊列

GCD中獲得串行有兩種途徑

  • 使用主隊列(跟主線程相關聯的隊列)
    主隊列是GCD自帶的一種特殊的串行隊列,放在主隊列中的任務,都會放到主線程中執行。提交給main queue的任務可能不會立馬被執行,而是在主線程的Run Loop檢測到有dispatch 提交過來的任務時纔會執行。
    使用dispatch_get_main_queue()函數獲得主隊列
    注意:如果把任務放到主隊列中進行處理,那麼不論處理函數是異步還是同步,都不會開闢新的線程。
    示例:dispatch_queue_t queue = dispatch_get_main_queue();

  • 使用dispatch_queue_create函數創建串行隊列
    說明:dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr); // 隊列名稱, 隊列屬性,一般用NULL即可,也可以設置參數DISPATCH_QUEUE_SERIAL
    示例:dispatch_queue_t queue = dispatch_queue_create(“隊列”, NULL); // 創建
    注意:非ARC需要釋放手動創建的隊列,dispatch_release(queue);

2.2 併發隊列

GCD中同樣提供了兩種途徑獲得串行

  • 使用全局隊列
    GCD默認已經提供了全局的併發隊列,供整個應用使用,不需要手動創建。也就是說我們可以直接提交給這個queue,任務會在非主線程的其他線程執行。
    使用dispatch_get_global_queue函數獲得全局的併發隊列
    說明:dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority,unsigned long flags);
    示例:dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    第一個參數爲優先級,這裏選擇默認的。獲取一個全局的默認優先級的併發隊列。
    說明:全局併發隊列的優先級
    define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
    define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默認(中)
    define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
    define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 後臺
    第二個參數參數是留給以後用的,暫時用不上,傳個0。

獲得全局併發Dispatch Queue (concurrent dispatch queue)
1. 併發dispatch queue可以同時並行地執行多個任務,不過併發queue仍然按先進先出的順序來啓動任務。併發queue會在之前的任務完成之前就出列下一個任務並開始執行。併發queue同時執行的任務數量會根據應用和系統動態變化,各種因素包括:可用核數量、其它進程正在執行的工作數量、其它串行dispatch queue中優先任務的數量等。
2. 系統給每個應用提供三個併發dispatch queue,整個應用內全局共享,三個queue的區別是優先級。你不需要顯式地創建這些queue,使用dispatch_get_global_queue函數來獲取這三個queue
第一個參數用於指定優先級,分別使用DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW兩個常量來獲取高和低優先級的兩個queue;第二個參數目前未使用到,默認0即可
3. 雖然dispatch queue是引用計數的對象,但你不需要retain和release全局併發queue。因爲這些queue對應用是全局的,retain和release調用會被忽略。你也不需要存儲這三個queue的引用,每次都直接調用dispatch_get_global_queue獲得queue就行了。

  • 使用dispatch_queue_create函數創建並行隊列
    和獲取串行隊列一樣,我們同樣可以使用dispatch_queue_create函數創建並行隊列,區別在於填入的第二個單數和獲取串行隊列的有所不同。
    說明:dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr); // 隊列名稱, 隊列屬性設置參數爲DISPATCH_QUEUE_CONCURRENT
    示例:dispatch_queue_t queue = dispatch_queue_create(“隊列”, DISPATCH_QUEUE_CONCURRENT);
    注意:非ARC需要釋放手動創建的隊列,dispatch_release(queue);

2.3 queue的內存管理

ARC環境下,不需要手動編寫代碼。 非ARC環境下需要使用 dispatch_retain()和 dispatch_release() 管理queue的引用計數。原則如同NSObject對象,計數爲0時銷燬queue。
代碼實例:

dispatch_queue_t q_1 =  dispatch_queue_create("task3.queue.1",DISPATCH_QUEUE_SERIAL);
                  dispatch_release(q_1);

3. 提交任務

GCD中有2個用來執行任務的函數,兩個函數的作用就是把右邊的參數(任務)提交給左邊的參數(隊列)進行執行。

1. 用同步的方式執行任務 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
2. 用異步的方式執行任務 dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
  參數說明:queue,任務執行的隊列
                    block,需要執行任務

同步和異步的區別:

  • 同步:在當前線程中執行,會等待任務完成,卡死當前線程
  • 異步:在另一條線程中執行,不需要等待任務完成,不會對當前線程產生影響

3.1 同步提交

同步提交函數爲dispatch_sync(),兩個參數:

  • 第一個參數表示提交到的queue
  • 第二個參數表示任務詳情

    這裏用block的方式描述一個任務,原因很簡單,Block也是純C實現的,而平時使用的Invocation或者target+selector方式都是面向對象的。
    注意,同步提交任務後,先執行完block,然後dispatch_sync()返回,這時候如果同步提交的任務過長會導致主線程的卡死。

3.2 異步提交

異步提交函數爲dispatch_async(),兩個參數:

  • 第一個參數表示提交到的queue
  • 第二個參數表示任務詳情

    異步提交任務後,dispatch_async()函數直接返回,無需等待block執行結束。不會導致主線程的卡死。

3.3 同時提交多次任務

同時提交多個任務給queue的函數很簡單:

 dispatch_apply(size_titerations,dispatch_queue_tqueue,void(^block)(size_t));
三個參數分別是任務數,目標queue,任務描述

這裏注意描述任務的block會重複多次調用,每次會給我們一個參數,表示任務次序。多個任務的執行順序取決於添加隊列queue的形式,如果目標queue是串行的,那麼任務會依次執行,如果queue是並行的,那麼任務會併發的執行,打印的順序就會被打亂。

3.4 死鎖

同步提交在某種情況下會造成死鎖,即卡死。 示例1:

dispatch_queue_t mainQ = dispatch_get_main_queue();
dispatch_sync(mainQ, ^{
    NSLog(@"----");
});
NSLog(@"OK");

示例2:

dispatch_queue_t q_1 =  dispatch_queue_create("task3.queue.1",DISPATCH_QUEUE_SERIAL);
dispatch_async(q_1, ^{

    NSLog(@"current is in q_1");
// q_1 blcok q_1 q_1 dispatch_sync
block block dispatch_sync
dispatch_sync(q_1, ^{
NSLog(@"this is sync ");

});
   NSLog(@"this is sync ????");
});

綜合上面兩個示例代碼,結論顯而易見:在一個串行隊列執行的代碼中,如果向此隊列同步提交一個任務,會造成死鎖。爲了避免出現死鎖的情況,要求我們避免在串行隊列中同步提交任務給本身的隊列。

3.5 各種隊列的執行效果

這裏寫圖片描述

3.6 補充說明

學習GCD時,有4個術語比較容易混淆:同步、異步、併發、串行。

同步和異步決定了要不要開啓新的線程

  • 同步:在當前線程中執行任務,不具備開啓新線程的能力
  • 異步:在新的線程中執行任務,具備開啓新線程的能力

併發和串行決定了任務的執行方式

  • 併發:多個任務併發(同時)執行
  • 串行:一個任務執行完畢後,再執行下一個任務

3.7 代碼示例

(1)用異步函數往併發隊列中添加任務

//1.獲得全局的併發隊列
    dispatch_queue_t queue =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
     //2.添加任務到隊列中,就可以執行任務
     //異步函數:具備開啓新線程的能力
     dispatch_async(queue, ^{
         NSLog(@"下載圖片1----%@",[NSThread currentThread]);
     });
     dispatch_async(queue, ^{
         NSLog(@"下載圖片2----%@",[NSThread currentThread]);
     });
     dispatch_async(queue, ^{
         NSLog(@"下載圖片2----%@",[NSThread currentThread]);
     });
     //打印主線程
    NSLog(@"主線程----%@",[NSThread mainThread]);

這裏寫圖片描述
總結:同時開啓三個子線程

(2)用異步函數往串行隊列中添加任務

//打印主線程
     NSLog(@"主線程----%@",[NSThread mainThread]);

     //創建串行隊列
     dispatch_queue_t  queue= dispatch_queue_create("wendingding", NULL);
     //第一個參數爲串行隊列的名稱,是c語言的字符串
     //第二個參數爲隊列的屬性,一般來說串行隊列不需要賦值任何屬性,所以通常傳空值(NULL)

    //2.添加任務到隊列中執行
    dispatch_async(queue, ^{
         NSLog(@"下載圖片1----%@",[NSThread currentThread]);
     });
     dispatch_async(queue, ^{
         NSLog(@"下載圖片2----%@",[NSThread currentThread]);
     });
     dispatch_async(queue, ^{
         NSLog(@"下載圖片2----%@",[NSThread currentThread]);
     });

這裏寫圖片描述
總結:會開啓線程,但是隻開啓一個線程

(3)用同步函數往併發隊列中添加任務

//打印主線程
    NSLog(@"主線程----%@",[NSThread mainThread]);

     //創建串行隊列
     dispatch_queue_t  queue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

     //2.添加任務到隊列中執行
     dispatch_sync(queue, ^{
         NSLog(@"下載圖片1----%@",[NSThread currentThread]);
     });
     dispatch_sync(queue, ^{
         NSLog(@"下載圖片2----%@",[NSThread currentThread]);
     });
     dispatch_sync(queue, ^{
         NSLog(@"下載圖片3----%@",[NSThread currentThread]);
     });
 }

這裏寫圖片描述
總結:不會開啓新的線程,併發隊列失去了併發的功能

(4)用同步函數往串行隊列中添加任務

NSLog(@"用同步函數往串行隊列中添加任務");
     //打印主線程
     NSLog(@"主線程----%@",[NSThread mainThread]);

     //創建串行隊列
     dispatch_queue_t  queue= dispatch_queue_create("wendingding", NULL);
     //2.添加任務到隊列中執行
     dispatch_sync(queue, ^{
         NSLog(@"下載圖片1----%@",[NSThread currentThread]);
     });
     dispatch_sync(queue, ^{
         NSLog(@"下載圖片2----%@",[NSThread currentThread]);
     });
     dispatch_sync(queue, ^{
         NSLog(@"下載圖片3----%@",[NSThread currentThread]);
     });
 }

這裏寫圖片描述
總結:不會開啓新的線程

(5)補充
補充:隊列名稱的作用:
將來調試的時候,可以看得出任務是在哪個隊列中執行的。


4. queue的暫停和繼續

我們可以使用dispatch_suspend函數暫停一個queue以阻止它執行尚未block對象,使用 dispatch_resume函數繼續dispatch queue。掛起和繼續是異步的,而且只在執行block之間(比如在執行一個新的block之前或之後)生效。掛起一個queue不會導致正在執行的block停止。特別強調,需要我們保證掛起隊列和重啓隊列的函數成對調用。
在非ARC中使用時需要注意:調用dispatch_suspend會增加queue的引用計數,調用 dispatch_resume則減少queue的引用計數。當引用計數大於0時,queue就保持掛起狀態。因此你必須對應地調用suspend和resume函數。

 // 掛起與重啓任務
          dispatch_suspend(globe_queue);
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
               dispatch_resume(globe_queue);
          });

5. Dispatch Group

   什麼是dispatch group,就像我們在NSOperation中添加依賴一樣,如果我們一個任務需要等待其他一些任務完成才能執行時,我們使用dispatch group是最輕鬆的解決方式。

使用隊列組可以讓圖片1和圖片2的下載任務同時進行,且當兩個下載任務都完成的時候回到主線程進行顯示。

5.1 設置任務執行順序

   GCD設置任務執行順序是通過Dispatch Group實現的。我們以吃火鍋爲例:
  • 首先創建group 與任務隊列

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t globleQ = dispatch_get_global_queue(0, 0);

  • 提交任務,並且把任務添加到group中

    dispatch_group_async(group, globleQ, ^{});
    注意:提交任務到group是沒有同步提交的,只有異步提交

  • 提交最終任務到group,同樣爲異步提交,

    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{});

5.2 時間延遲

  • dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    使當前線程堵塞,一直等待group所有任務結束:

  • dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (300 *NSEC_PER_SEC));

  • dispatch_group_wait(group, time); NSLog(@”“);

    也可以自定義超時時間,只等待一定的時間之後group內的任務沒有執行的直接停止


6. GCD的常用方法

6.1 延遲執行

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//延遲執行的方法
});
說明:在5秒鐘之後,執行block中的代碼段。
參數說明:

6.2 一次性執行(常用與單例設置)

使用dispatch_once一次性代碼
使用dispatch_once函數能保證某段代碼在程序運行過程中只被執行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只執行1次的代碼(這裏面默認是線程安全的)
});
整個程序運行過程中,只會執行一次。

7. 小結

說明:同步函數不具備開啓線程的能力,無論是什麼隊列都不會開啓線程;異步函數具備開啓線程的能力,開啓幾條線程由隊列決定(串行隊列只會開啓一條新的線程,併發隊列會開啓多條線程)。
同步函數
(1)併發隊列:不會開線程
(2)串行隊列:不會開線程
異步函數
(1)併發隊列:能開啓N條線程
(2)串行隊列:開啓1條線程
補充: 在非ARC工程中,凡是函數中,各種函數名中帶有create\copy\new\retain等字眼,都需要在不需要使用這個數據的時候進行release。
GCD的數據類型在ARC的環境下不需要再做release。
CF(core Foundation)的數據類型在ARC環境下還是需要做release。
異步函數具備開線程的能力,但不一定會開線程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章