GCD的基本介紹及常見用法和應用場景

一、隊列和任務

初學GCD的時候,肯定會糾結一些看似很關鍵但卻毫無意義的問題。比如:GCD和線程到底什麼關係;異步任務到底在哪個線程工作;隊列到底是個什麼東西;mian queue和main thread到底搞什麼名堂等等。現在,這些我們直接略過(最後拾遺中會談一下),蘋果既然推薦使用GCD,那麼爲什麼還要糾結於線程呢?需要關注的只有兩個概念:隊列、任務。

1、隊列

調度隊列是一個對象,它會以first-in、first-out的方式管理您提交的任務。GCD有三種隊列類型:

串行隊列,串行隊列將任務以先進先出(FIFO)的順序來執行,所以串行隊列經常用來做訪問某些特定資源的同步處理。你可以也根據需要創建多個隊列,而這些隊列相對其他隊列都是併發執行的。換句話說,如果你創建了4個串行隊列,每一個隊列在同一時間都只執行一個任務,對這四個任務來說,他們是相互獨立且併發執行的。如果需要創建串行隊列,一般用dispatch_queue_create這個方法來實現,並指定隊列類型DISPATCH_QUEUE_SERIAL。


並行隊列,併發隊列雖然是能同時執行多個任務,但這些任務仍然是按照先到先執行(FIFO)的順序來執行的。併發隊列會基於系統負載來合適地選擇併發執行這些任務。併發隊列一般指的就是全局隊列(Global queue),進程中存在四個全局隊列:高、中(默認)、低、後臺四個優先級隊列,可以調用dispatch_get_global_queue函數傳入優先級來訪問隊列。當然我們也可以用dispatch_queue_create,並指定隊列類型DISPATCH_QUEUE_CONCURRENT,來自己創建一個併發隊列。


主隊列,與主線程功能相同。實際上,提交至main queue的任務會在主線程中執行。main queue可以調用dispatch_get_main_queue()來獲得。因爲main queue是與主線程相關的,所以這是一個串行隊列。和其它串行隊列一樣,這個隊列中的任務一次只能執行一個。它能保證所有的任務都在主線程執行,而主線程是唯一可用於更新 UI 的線程。
額外說一句,上面也說過,隊列間的執行是並行的,但是也存在一些限制。比如,並行執行的隊列數量受到內核數的限制,無法真正做到大量隊列並行執行;比如,對於並行隊列中的全局隊列而言,其存在優先級關係,執行的時候也會遵循其優先順序,而不是並行。

2、任務

linux內核中的任務的定義是描述進程的一種結構體,而GCD中的任務只是一個代碼塊,它可以指一個block或者函數指針。根據這個代碼塊添加進入隊列的方式,將任務分爲同步任務和異步任務:

同步任務,使用dispatch_sync將任務加入隊列。將同步任務加入串行隊列,會順序執行,一般不這樣做並且在一個任務未結束時調起其它同步任務會死鎖。將同步任務加入並行隊列,會順序執行,但是也沒什麼意義。

異步任務,使用dispatch_async將任務加入隊列。將異步任務加入串行隊列,會順序執行,並且不會出現死鎖問題。將異步任務加入並行隊列,會並行執行多個任務,這也是我們最常用的一種方式。
3、簡單應用

// 隊列的創建,queue1:中(默認)優先級的全局並行隊列、queue2:主隊列、queue3:未指定type則爲串行隊列、queue4:指定串行隊列、queue5:指定並行隊列
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue2 = dispatch_get_main_queue();
dispatch_queue_t queue3 = dispatch_queue_create("queue3", NULL);
dispatch_queue_t queue4 = dispatch_queue_create("queue4", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue5 = dispatch_queue_create("queue5", DISPATCH_QUEUE_CONCURRENT);

// 隊列中添加異步任務
dispatch_async(queue1, ^{
// 任務
...
});

// 隊列中添加同步任務
dispatch_sync(queue1, ^{
// 任務
...
});

二、GCD常見用法和應用場景
1、dispatch_async
一般用法

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 一個異步的任務,例如網絡請求,耗時的文件操作等等
    ...
    dispatch_async(dispatch_get_main_queue(), ^{
        // UI刷新
        ...
    });
});

應用場景
這種用法非常常見,比如開啓一個異步的網絡請求,待數據返回後返回主隊列刷新UI;又比如請求圖片,待圖片返回刷新UI等等。


2、dispatch_after
一般用法

dispatch_queue_t queue= dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), queue, ^{
    // 在queue裏面延遲執行的一段代碼
    ...
});

應用場景
這爲我們提供了一個簡單的延遲執行的方式,比如在view加載結束延遲執行一個動畫等等。


3、dispatch_once
一般用法

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只執行一次的任務
    ...
});

應用場景
可以使用其創建一個單例,也可以做一些其他只執行一次的代碼,比如做一個只能點一次的button(好像沒啥用)。


4、dispatch_group
一般用法

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{
    // 異步任務1
});

dispatch_group_async(group, queue, ^{
    // 異步任務2
});

// 等待group中多個異步任務執行完畢,做一些事情,介紹兩種方式

// 方式1(不好,會卡住當前線程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
...

// 方式2(比較好)
dispatch_group_notify(group, mainQueue, ^{
    // 任務完成後,在主隊列中做一些操作
    ...
});

應用場景
上述的一種方式,可以適用於自己維護的一些異步任務的同步問題;但是對於已經封裝好的一些庫,比如AFNetworking等,我們不獲取其異步任務的隊列,這裏可以通過一種計數的方式控制任務間同步,下面爲解決單界面多接口的一種方式。

// 兩個請求和參數爲我項目裏面的不用在意。

// 計數+1
dispatch_group_enter(group);
[JDApiService getActivityDetailWithActivityId:self.activityId Location:stockAddressId SuccessBlock:^(NSDictionary *userInfo) {
    // 數據返回後一些處理
    ...

    // 計數-1
    dispatch_group_leave(group);
} FailureBlock:^(NSError *error) {
    // 數據返回後一些處理
    ...

    // 計數-1
    dispatch_group_leave(group);
}];

// 計數+1
dispatch_group_enter(group);
[JDApiService getAllCommentWithActivityId:self.activityId PageSize:3 PageNum:self.commentCurrentPage SuccessBlock:^(NSDictionary *userInfo) {
    // 數據返回後一些處理
    ...

    // 計數-1
    dispatch_group_leave(group);
} FailureBlock:^(NSError *error) {
    // 數據返回後一些處理
    ...

    // 計數-1
    dispatch_group_leave(group);
}];

// 其實用計數的說法可能不太對,但是就這麼理解吧。會在計數爲0的時候執行dispatch_group_notify的任務。
dispatch_group_notify(group, mainQueue, ^{
    // 一般爲回主隊列刷新UI
    ...
});

5、dispatch_barrier_async
一般用法

// dispatch_barrier_async的作用可以用一個詞概括--承上啓下,它保證此前的任務都先於自己執行,此後的任務也遲於自己執行。本例中,任務4會在任務123都執行完之後執行,而任務56會等待任務4執行完後執行。

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    // 任務1
    ...
});
dispatch_async(queue, ^{
    // 任務2
    ...
});
dispatch_async(queue, ^{
    // 任務3
    ...
});
dispatch_barrier_async(queue, ^{
    // 任務4
    ...
});
dispatch_async(queue, ^{
    // 任務5
    ...
});
dispatch_async(queue, ^{
    // 任務6
    ...
});

應用場景
和dispatch_group類似,dispatch_barrier也是異步任務間的一種同步方式,可以在比如文件的讀寫操作時使用,保證讀操作的準確性。另外,有一點需要注意,dispatch_barrier_sync和dispatch_barrier_async只在自己創建的併發隊列上有效,在全局(Global)併發隊列、串行隊列上,效果跟dispatch_(a)sync效果一樣。


6、dispatch_apply
一般用法

// for循環做一些事情,輸出0123456789
for (int i = 0; i < 10; i ++) {
    NSLog(@"%d", i);
}

// dispatch_apply替換(當且僅當處理順序對處理結果無影響環境),輸出順序不定,比如1098673452
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*! dispatch_apply函數說明
*
*  @brief  dispatch_apply函數是dispatch_sync函數和Dispatch Group的關聯API
*         該函數按指定的次數將指定的Block追加到指定的Dispatch Queue中,並等到全部的處理執行結束
*
*  @param 10    指定重複次數  指定10次
*  @param queue 追加對象的Dispatch Queue
*  @param index 帶有參數的Block, index的作用是爲了按執行的順序區分各個Block
*
*/
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zu", index);
});

應用場景
那麼,dispatch_apply有什麼用呢,因爲dispatch_apply並行的運行機制,效率一般快於for循環的類串行機制(在for一次循環中的處理任務很多時差距比較大)。比如這可以用來拉取網絡數據後提前算出各個控件的大小,防止繪製時計算,提高表單滑動流暢性,如果用for循環,耗時較多,並且每個表單的數據沒有依賴關係,所以用dispatch_apply比較好。


7、dispatch_suspend和dispatch_resume
一般用法

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_suspend(queue); //暫停隊列queue
dispatch_resume(queue);  //恢復隊列queue

應用場景
這種用法我還沒有嘗試過,不過其中有個需要注意的點。這兩個函數不會影響到隊列中已經執行的任務,隊列暫停後,已經添加到隊列中但還沒有執行的任務不會執行,直到隊列被恢復。


8、dispatch_semaphore_signal
一般用法

// dispatch_semaphore_signal有兩類用法:a、解決同步問題;b、解決有限資源訪問(資源爲1,即互斥)問題。
// dispatch_semaphore_wait,若semaphore計數爲0則等待,大於0則使其減1。
// dispatch_semaphore_signal使semaphore計數加1。

// a、同步問題:輸出肯定爲1、2、3。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore1 = dispatch_semaphore_create(1);
dispatch_semaphore_t semaphore2 = dispatch_semaphore_create(0);
dispatch_semaphore_t semaphore3 = dispatch_semaphore_create(0);

dispatch_async(queue, ^{
    // 任務1
    dispatch_semaphore_wait(semaphore1, DISPATCH_TIME_FOREVER);
    NSLog(@"1\n");
    dispatch_semaphore_signal(semaphore2);
    dispatch_semaphore_signal(semaphore1);
});

dispatch_async(queue, ^{
    // 任務2
    dispatch_semaphore_wait(semaphore2, DISPATCH_TIME_FOREVER);
    NSLog(@"2\n");
    dispatch_semaphore_signal(semaphore3);
    dispatch_semaphore_signal(semaphore2);
});

dispatch_async(queue, ^{
    // 任務3
    dispatch_semaphore_wait(semaphore3, DISPATCH_TIME_FOREVER);
    NSLog(@"3\n");
    dispatch_semaphore_signal(semaphore3);
});

// b、有限資源訪問問題:for循環看似能創建100個異步任務,實質由於信號限制,最多創建10個異步任務。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);

應用場景
其實關於dispatch_semaphore_t,並沒有看到太多應用和資料解釋,我只能參照自己對linux信號量的理解寫了兩個用法,經測試確實相似。這裏,就不對一些死鎖問題進行討論了。


9 dispatch_set_context、dispatch_get_context和dispatch_set_finalizer_f
一般用法

// dispatch_set_context、dispatch_get_context是爲了向隊列中傳遞上下文context服務的。
// dispatch_set_finalizer_f相當於dispatch_object_t的析構函數。
// 因爲context的數據不是foundation對象,所以arc不會自動回收,一般在dispatch_set_finalizer_f中手動回收,所以一般講上述三個方法綁定使用。

- (void)test
{
    // 幾種創建context的方式
    // a、用C語言的malloc創建context數據。
    // b、用C++的new創建類對象。
    // c、用Objective-C的對象,但是要用__bridge等關鍵字轉爲Core Foundation對象。

    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    if (queue) {
        // "123"即爲傳入的context
        dispatch_set_context(queue, "123");
        dispatch_set_finalizer_f(queue, &xigou);
    }
    dispatch_async(queue, ^{
        char *string = dispatch_get_context(queue);
        NSLog(@"%s", string);
    });
}

// 該函數會在dispatch_object_t銷燬時調用。
void xigou(void *context)
{
    // 釋放context的內存(對應上述abc)

    // a、CFRelease(context);
    // b、free(context);
    // c、delete context;
}

應用場景
dispatch_set_context可以爲隊列添加上下文數據,但是因爲GCD是C語言接口形式的,所以其context參數類型是“void *”。需使用上述abc三種方式創建context,並且一般結合dispatch_set_finalizer_f使用,回收context內存。

發佈了26 篇原創文章 · 獲贊 7 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章