iOS多線程系列之三:GCD用法大全

##一、GCD簡介
GCD(Grand Central Dispatch) 偉大的中央調度系統,是蘋果爲多核並行運算提出的C語言併發技術框架。

GCD會自動利用更多的CPU內核;
會自動管理線程的生命週期(創建線程,調度任務,銷燬線程等);
程序員只需要告訴 GCD 想要如何執行什麼任務,不需要編寫任何線程管理代碼

一些專業術語

dispatch :派遣/調度
    
queue:隊列
    用來存放任務的先進先出(FIFO)的容器
sync:同步
    只是在當前線程中執行任務,不具備開啓新線程的能力
async:異步
    可以在新的線程中執行任務,具備開啓新線程的能力
concurrent:併發
    多個任務併發(同時)執行
串行:
    一個任務執行完畢後,再執行下一個任務

##二、GCD中的核心概念
###1.任務
任務就是要在線程中執行的操作。我們需要將要執行的代碼用block封裝好,然後將任務添加到隊列並指定任務的執行方式,等待CPU從隊列中取出任務放到對應的線程中執行。

 - queue:隊列
 - block:任務
// 1.用同步的方式執行任務
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

// 2.用異步的方式執行任務
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

// 3.GCD中還有個用來執行任務的函數
// 在前面的任務執行結束後它才執行,而且它後面的任務等它執行完成之後纔會執行
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

###2.隊列
隊列以先進先出按照執行方式(併發/串行)調度任務在對應的線程上執行;
隊列分爲:自定義隊列、主隊列和全局隊列;

<1>自定義隊列
自定義隊列又分爲:串行隊列和併發隊列

  • 串行隊列
    串行隊列一次只調度一個任務,一個任務完成後再調度下一個任務
// 1.使用dispatch_queue_create函數創建串行隊列
////OC
// 創建串行隊列(隊列類型傳遞NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", NULL);
////Swift
let serialQueue = DispatchQueue(label: "serialQueue")

// 2.獲得主隊列
////OC
dispatch_queue_t mainQueue = dispatch_get_main_queue();
////Swift
let mainQueue = DispatchQueue.main
注意:主隊列是GCD自帶的一種特殊的串行隊列,放在主隊列中的任務,都會放到主線程中執行。
  • 併發隊列
    併發隊列可以同時調度多個任務,調度任務的方式,取決於執行任務的函數;併發功能只有在異步的(dispatch_async)函數下才有效;異步狀態下,開啓的線程上限由GCD底層決定。
// 1.使用dispatch_queue_create函數創建隊列
dispatch_queue_t
dispatch_queue_create(const char *label, // 隊列名稱,該名稱可以協助開發調試以及崩潰分析報告 
dispatch_queue_attr_t attr); // 隊列的類型

// 2.創建併發隊列
////OC
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
////Swift
let concurrentQueue = DispatchQueue(label: "concurrentQueue",attributes:.concurrent)

自定義隊列在MRC開發時需要使用dispatch_release釋放隊列

#if !__has_feature(objc_arc)
    dispatch_release(queue);
#endif

<2>主隊列
主隊列負責在主線程上調度任務,如果在主線程上有任務執行,會等待主線程空閒後再調度任務執行。
主隊列用於UI以及觸摸事件等的操作,我們在進行線程間通信,通常是返回主線程更新UI的時候使用到

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 耗時操作
    // ...
    //放回主線程的函數
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主線程更新 UI
    });
});

<3>全局併發隊列

全局併發隊列是由蘋果API提供的,方便程序員使用多線程。

//使用dispatch_get_global_queue函數獲得全局的併發隊列
dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority, unsigned long flags);
// dispatch_queue_priority_t priority(隊列的優先級 )
// unsigned long flags( 此參數暫時無用,用0即可 )

//獲得全局併發隊列
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 // 後臺優先級

然而,iOS8 開始使用 QOS(quality of service
) 替代了原有的優先級。獲取全局併發隊列時,直接傳遞 0,可以實現 iOS 7 & iOS 8 later 的適配。

//像這樣
dispatch_get_global_queue(0, 0);

<4>全局併發隊列與併發隊列的區別

全局併發隊列與併發隊列的調度方法相同
全局併發隊列沒有隊列名稱
在MRC開發中,全局併發隊列不需要手動釋放

<5>QOS (服務質量) iOS 8.0 推出

QOS_CLASS_USER_INTERACTIVE:用戶交互,會要求 CPU 儘可能地調度此任務,耗時操作不應該使用此服務質量
QOS_CLASS_USER_INITIATED:用戶發起,比 QOS_CLASS_USER_INTERACTIVE 的調度級別低,但是比默認級別高;耗時操作同樣不應該使用此服務質量;如果用戶希望任務儘快執行完畢返回結果,可以選擇此服務質量;
QOS_CLASS_DEFAULT:默認,此 QOS 不是爲添加任務準備的,主要用於傳送或恢復由系統提供的 QOS 數值時使用
QOS_CLASS_UTILITY:實用,耗時操作可以使用此服務質量;
QOS_CLASS_BACKGROUND:後臺,指定任務以最節能的方式運行
QOS_CLASS_UNSPECIFIED:沒有指定 QOS

###3.執行任務的函數
<1>同步(dispatch_sync)

執行完這一句代碼,再執行後續的代碼就是同步

任務被添加到隊列後,會當前線程被調度;隊列中的任務同步執行完成後,纔會調度後續任務。-在主線程中,向主隊列添加同步任務,會造成死鎖
-在其他線程中,向主隊列向主隊列添加同步任務,則會在主線程中同步執行。
具體是否會造成死鎖,以及死鎖的原因,還需要針對具體的情況分析,理解隊列和執行任務的函數纔是關鍵。實際開發中一般只要記住常用的組合就可以了。
我們可以利用同步的機制,建立任務之間的依賴關係
例如:

用戶登錄後,才能夠併發下載多部小說
只有“用戶登錄”任務執行完成之後,多個下載小說的任務才能夠“異步”執行
所有下載任務都依賴“用戶登錄”

<2>異步(dispatch_async)

不必等待這一句代碼執行完,就執行下一句代碼就是異步

異步是多線程的代名詞,當任務被添加到主隊列後,會等待主線程空閒時纔會調度該任務;添加到其他線程時,會開啓新的線程調度任務。
<3>以函數指針的方式調度任務
函數指針的調用方式有兩種,同樣是同步和異步;函數指針的傳遞類似於 pthread。

dispatch_sync_f
dispatch_async_f

函數指針調用在實際開發中幾乎不用,只是有些面試中會問到,dispatch + block 纔是 gcd 的主流!
###4.開發中如何選擇隊列
選擇隊列當然是要先了解隊列的特點
串行隊列:對執行效率要求不高,對執行順序要求高,性能消耗小
併發隊列:對執行效率要求高,對執行順序要求不高,性能消耗大
如果不想兼顧 MRC 中隊列的釋放,建議選擇使用全局隊列 + 異步任務。
##三、GCD的其他用法
###1.延時執行
參數1:從現在開始經過多少納秒,參數2:調度任務的隊列,參數3:異步執行的任務
dispatch_after(when, queue, block)
例如:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒後異步執行這裏的代碼...
});

###2.一次性執行
應用場景:保證某段代碼在程序運行過程中只被執行一次,在單例設計模式中被廣泛使用。

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

###3.調度組(隊列組)
應用場景:需要在多個耗時操作執行完畢之後,再統一做後續處理

//創建調度組
dispatch_group_t group = dispatch_group_create();
//將調度組添加到隊列,執行 block 任務
dispatch_group_async(group, queue, block);
//當調度組中的所有任務執行結束後,獲得通知,統一做後續操作
dispatch_group_notify(group, dispatch_get_main_queue(), block);

例如:

// 分別異步執行2個耗時的操作、2個異步操作都執行完畢後,再回到主線程執行操作
dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 執行1個耗時的異步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 執行1個耗時的異步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 等前面的異步操作都執行完畢後,回到主線程...
});

###4.定時器

//創建代碼
dispatch_source_t CreateDispatchTimer(uint64_t interval,
  uint64_t leeway,
  dispatch_queue_t queue,
  dispatch_block_t block)
{
 dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
       0, 0, queue);
 if (timer)
 {
 dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
 dispatch_source_set_event_handler(timer, block);
 dispatch_resume(timer);
 }
 return timer;
}
  • Dispatch Source Timer 是間隔定時器,也就是說每隔一段時間間隔定時器就會觸發。在 NSTimer 中要做到同樣的效果需要手動把 repeats 設置爲 YES。

  • dispatch_source_set_timer 中第二個參數,當我們使用dispatch_time 或者 DISPATCH_TIME_NOW 時,系統會使用默認時鐘來進行計時。然而當系統休眠的時候,默認時鐘是不走的,也就會導致計時器停止。使用 dispatch_walltime 可以讓計時器按照真實時間間隔進行計時。

dispatch_time與dispatch_walltime 區別
使用第一個函數創建的是一個相對的時間,第一個參數開始時間參考的是當前系統的時鐘,當 device 進入休眠之後,系統的時鐘也會進入休眠狀態, 第一個函數同樣被掛起; 假如 device 在第一個函數開始執行後10分鐘進入了休眠狀態,那麼這個函數同時也會停止執行,當你再次喚醒 device 之後,該函數同時被喚醒,但是事件的觸發就變成了從喚醒 device 的時刻開始,1小時之後
而第二個函數則不同,他創建的是一個絕對的時間點,一旦創建就表示從這個時間點開始,1小時之後觸發事件,假如 device 休眠了10分鐘,當再次喚醒 device 的時候,計算時間間隔的時間起點還是 開始時就設置的那個時間點, 而不會受到 device 是否進入休眠影響

  • dispatch_source_set_timer 的第四個參數 leeway 指的是一個期望的容忍時間,將它設置爲 1 秒,意味着系統有可能在定時器時間到達的前 1 秒或者後 1 秒才真正觸發定時器。在調用時推薦設置一個合理的 leeway 值。需要注意,就算指定 leeway 值爲 0,系統也無法保證完全精確的觸發時間,只是會儘可能滿足這個需求。

  • event handler block 中的代碼會在指定的 queue 中執行。當 queue 是後臺線程的時候,dispatch timer 相比 NSTimer 就好操作一些了。因爲 NSTimer 是需要 Runloop 支持的,如果要在後臺 dispatch queue 中使用,則需要手動添加 Runloop。使用 dispatch timer 就簡單很多了。

  • dispatch_source_set_event_handler 這個函數在執行完之後,block 會立馬執行一遍,後面隔一定時間間隔再執行一次。而 NSTimer 第一次執行是到計時器觸發之後。這也是和 NSTimer 之間的一個顯著區別。
    停止 Timer

停止 Dispatch Timer 有兩種方法,一種是使用 dispatch_suspend,另外一種是使用 dispatch_source_cancel。

dispatch_suspend 嚴格上只是把 Timer 暫時掛起,它和 dispatch_resume 是一個平衡調用,兩者分別會減少和增加 dispatch 對象的掛起計數。當這個計數大於 0 的時候,Timer 就會執行。在掛起期間,產生的事件會積累起來,等到 resume 的時候會融合爲一個事件發送。
注意
dispatch_source_cancel 則是真正意義上的取消 Timer。被取消之後如果想再次執行 Timer,只能重新創建新的 Timer。這個過程類似於對 NSTimer 執行 invalidate。

關於取消 Timer,另外一個很重要的注意事項:dispatch_suspend 之後的 Timer,是不能被釋放的!因此使用 dispatch_suspend 時,Timer 本身的實例需要一直保持。使用 dispatch_source_cancel 則沒有這個限制。

下面的代碼會引起崩潰:
- (void)stopTimer
{
 dispatch_suspend(_timer);//EXC_BAD_INSTRUCTION 崩潰
 //dispatch_source_cancel(_timer);//OK
 _timer = nil; // 
}

##四、基於GCD的單例模式

作用:
可以保證在程序運行過程,一個類只有一個實例,而且該實例易於供外界訪問。從而方便地控制了實例個數,並節約系統資源
使用場合:
在整個應用程序中,共享一份資源(這份資源只需要創建初始化1次)

實現方法
重寫實現

// 1.在.m中保留一個全局的static的實例
static id _instance;

// 2.重寫allocWithZone:方法,在這裏創建唯一的實例(注意線程安全)
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    return _instance;
}

// 3.提供1個類方法讓外界訪問唯一的實例
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}

// 4.實現copyWithZone:方法
- (id)copyWithZone:(struct _NSZone *)zone
{
    return _instance;
}

宏實現

// .h文件
#define SingletonH(name) + (instancetype)shared##name;

// .m文件
#define SingletonM(name) 
static id _instance; 
 
+ (instancetype)allocWithZone:(struct _NSZone *)zone 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        _instance = [super allocWithZone:zone]; 
    }); 
    return _instance; 
} 
 
+ (instancetype)shared##name 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        _instance = [[self alloc] init]; 
    }); 
    return _instance; 
} 
 
- (id)copyWithZone:(NSZone *)zone 
{ 
    return _instance; 
}

##五、如何取消GCD任務
有一部分人說GCD無法取消任務,也有人站出反對說話不負責任。那麼我們先來看看他提供的方案:return就可以正常結束一段代碼

- (void)viewDidLoad {
    [super viewDidLoad];

    [self gcdTest];
}


- (void)gcdTest{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 模擬耗時操作
        for (long i=0; i<100000; i++) {
            NSLog(@"i:%ld",i);
            sleep(1);
            // 山不過來,我就過去
            if (gcdFlag==YES) {
                NSLog(@"收到gcd停止信號");
                return ;
            }
        };
    });

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"發出停止gcd信號!");
        gcdFlag = YES;
    });
}

GCD中的定時器

//0.創建一個隊列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    //1.創建一個GCD的定時器
    /*
     第一個參數:說明這是一個定時器
     第四個參數:GCD的回調任務添加到那個隊列中執行,如果是主隊列則在主線程執行
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

    //2.設置定時器的開始時間,間隔時間以及精準度

    //設置開始時間,三秒鐘之後調用
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
    //設置定時器工作的間隔時間
    uint64_t intevel = 1.0 * NSEC_PER_SEC;

    /*
     第一個參數:要給哪個定時器設置
     第二個參數:定時器的開始時間DISPATCH_TIME_NOW表示從當前開始
     第三個參數:定時器調用方法的間隔時間
     第四個參數:定時器的精準度,如果傳0則表示採用最精準的方式計算,如果傳大於0的數值,則表示該定時切換i可以接收該值範圍內的誤差,通常傳0
     該參數的意義:可以適當的提高程序的性能
     注意點:GCD定時器中的時間以納秒爲單位(面試)
     */

    dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);

    //3.設置定時器開啓後回調的方法
    /*
     第一個參數:要給哪個定時器設置
     第二個參數:回調block
     */
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"------%@",[NSThread currentThread]);
    });

    //4.執行定時器
    dispatch_resume(timer);

    //注意:dispatch_source_t本質上是OC類,在這裏是個局部變量,需要強引用
    self.timer = timer;

GCD定時器補充
/*
 DISPATCH_SOURCE_TYPE_TIMER         定時響應(定時器事件)
 DISPATCH_SOURCE_TYPE_SIGNAL        接收到UNIX信號時響應

 DISPATCH_SOURCE_TYPE_READ          IO操作,如對文件的操作、socket操作的讀響應
 DISPATCH_SOURCE_TYPE_WRITE         IO操作,如對文件的操作、socket操作的寫響應
 DISPATCH_SOURCE_TYPE_VNODE         文件狀態監聽,文件被刪除、移動、重命名
 DISPATCH_SOURCE_TYPE_PROC          進程監聽,如進程的退出、創建一個或更多的子線程、進程收到UNIX信號

 下面兩個都屬於Mach相關事件響應
    DISPATCH_SOURCE_TYPE_MACH_SEND
    DISPATCH_SOURCE_TYPE_MACH_RECV
 下面兩個都屬於自定義的事件,並且也是有自己來觸發
    DISPATCH_SOURCE_TYPE_DATA_ADD
    DISPATCH_SOURCE_TYPE_DATA_OR
 */
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章