一、隊列和任務
初學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會在任務1、2、3都執行完之後執行,而任務5、6會等待任務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內存。