iOS多執行緒GCD篇

Perfect_Dream發表於2019-03-04

首先,GCD的原始碼在這Grand Central Dispatch,如果想要深入的理解GCD的實現原理,最好還是下載一份原始碼慢慢的閱讀一下。
本文不會對GCD的底層原始碼進行剖析,只會總結一下應用層面的東西。
本文會涉及到的內容:

  • 什麼是GCD
  • GCD的基礎實現
  • GCD與其他多執行緒實現方式相比的優劣
  • GCD常用API的釋義、解析與應用
  • GCD的一些坑

一. 什麼是GCD

GCD的全稱是Grand Central Dispatch,是蘋果公司開發的多核心處理器編寫併發執行的程式。在Mac OS X10.6 和iOS4以及以上的系統可以使用。它是一套底層API,提供了一種新的方法來進行併發程式編寫。從基本功能上講,GCD有點像NSOperationQueue,他們都允許程式將任務切分為多個單一任務然後提交至工作佇列來併發地或者序列地執行。GCD比之NSOpertionQueue更底層更高效,並且它不是Cocoa框架的一部分。
除了程式碼的平行執行能力,GCD還提供高度整合的事件控制系統。可以設定控制程式碼來響應檔案描述符、mach ports(Mach port 用於 OS X上的程式間通訊)、程式、計時器、訊號、使用者生成事件。這些控制程式碼通過GCD來併發執行。

蘋果官方對GCD的解釋中有這樣一句話:

開發者要做的只是定義執行的任務並追加到適當的 Dispatch Queue 中。

所以在GCD中我們需要做的只是兩件事:1:定義任務;2:把任務加到佇列中。

  • GCD允許程式將任務切分成多個任務然後提交到一個佇列中併發執行或者序列執行。
  • GCD是非常高效的多執行緒開發方式。並且它並不是Cocoa框架的一部分。

GCD程式設計的核心就是dispatch佇列,dispatch block的執行最終都會放進某個佇列中去進行,下邊是GCD幾種佇列獲取方式:

  1. 主執行緒佇列(The main queue):提交至main queue的任務會在主執行緒中執行。
    • 可以呼叫dispatch_get_main_queue()來獲得。
    • 因為main queue是與主執行緒相關的,所以這是一個序列佇列。和UI相關的修改必須使用Main Queue。
  2. 全域性併發佇列(Global queue): 全域性併發佇列由整個程式共享,有高、中(預設)、低、後臺四個優先順序別。
    • 可以通過呼叫dispatch_get_global_queue函式來獲取(可以設定優先順序)
  3. 自定義佇列(Custom queue):
    • 並行佇列(Concurrent Dispatch Queue):全域性佇列是併發佇列,並由整個程式共享。
      • 可以併發的執行多個任務,但是執行完成的順序是隨機的。
      • 程式中存在三個全域性佇列:高、中(預設)、低三個優先順序佇列。可以呼叫dispatch_get_global_queue函式傳入優先順序來訪問佇列。
    • 序列佇列(Serial Dispatch Queue):也叫做使用者佇列(GCD並不這樣稱呼這種佇列, 但是沒有一個特定的名字來形容這種佇列,所以我們稱其為使用者佇列)。
      • 同一時間只執行一個任務,通常用於同步訪問特定的資源或資料。
      • 當你建立多個序列佇列的時候,每個佇列裡的任務都是序列執行的
      • 每個序列佇列與佇列之間是併發執行的。
      • 有點像傳統執行緒中的mutex。
  4. Group queue (佇列組):將多執行緒進行分組,最大的好處是可獲知所有執行緒的完成情況。
    • Group queue 可以通過呼叫dispatch_group_create()來獲取,通過 dispatch_group_notify,可以直接監聽組裡所有執行緒完成情況。

所以,在使用GCD的時候我們只需要搞清楚我們的任務是要同步執行還是非同步執行,是要序列還是並行,我們並不需要直接和執行緒打交道,只需要向建立好的佇列中新增需要被執行的程式碼塊就可以了。除了提供程式碼併發執行的能力,還提供了高度整合的事件控制系統。GCD自己在後端管理著一個可排程執行緒池,不僅決定著你的程式碼塊將在哪個執行緒被執行,它還根據可用的系統資源對這些執行緒進行管理。這樣就可以將開發者從執行緒管理的工作中解放出來,並且通過執行緒的集中管理,緩解了執行緒被大量建立的問題。
GCD的API很大程度是基於block,當然也可以脫離block來使用,比如使用函式指標和上下文指標等。實踐證明配合block使用時,GCD非常的簡單易用且能發揮最大的能力。

二. GCD的基礎實現

GCD的基本概念就是dispatch queue。dispatch queue是一個物件,它可以接受任務,並將任務以先到先執行的順序來執行。也就是FIFO佇列。dispatch queue可以是併發的或序列的。併發任務會像NSOperationQueue那樣基於系統負載來合適地併發進行,序列佇列同一時間只執行單一任務。
GCD是純c語言的,但它被組建成物件導向的風格。GCD物件被稱為dispatch object。Dispatch object像Cocoa物件一樣是引用計數的。使用dispatch_release和dispatch_retain函式來操作dispatch object的引用計數來進行記憶體管理。
在iOS 6.0以前,我們必須手動管理GCD物件的記憶體,使用(dispatch_retain,dispatch_release),ARC並不會去管理它們。但是iOS6.0以後就不需要了,ARC已經能夠管理GCD物件了,這時候,GCD物件就如同普通的OC物件一樣,不應該使用dispatch_retain ordispatch_release。

FIFO:

FIFO佇列稱為dispatch queue,FIFO就是 First In First Out的簡稱,翻譯過來就是先進先出。GCD保證先進來的任務先得到執行。

三. GCD與其他多執行緒實現方式相比的優劣

GCD的優勢:

  1. GCD很優雅,輕巧。比專門建立消耗資源的執行緒更加的實用且快速。
  2. GCD自動根據系統的負載來增減執行緒的數量,這就增加了計算效率。
  3. GCD自動根據系統負載來增減執行緒數量,這就減少了上下文切換以及增加了計算效率。
  4. GCD 能通過推遲昂貴計算任務並在後臺執行它們來改善你的應用的響應效能。
  5. GCD 提供一個易於使用的併發模型而不僅僅只是鎖和執行緒,以幫助我們避開併發陷阱。
  6. GCD 具有在常見模式(例如單例)上用更高效能的原語優化你的程式碼的潛在能力。
  7. GCD 會自動利用更多的CPU
  8. 只需要告訴 GCD 想要執行什麼任務,不需要編寫任何執行緒管理程式碼
  9. GCD 會自動管理執行緒的生命週期(建立執行緒、排程任務、銷燬執行緒)

GCD提供很多超越傳統多執行緒程式設計的優勢:

  1. GCD比之NSThread跟簡單易用。由於GCD基於work unit而非像thread那樣基於運算,所以GCD可以控制諸如等待任務結束、監視檔案描述符、週期執行程式碼以及工作掛起等任務。基於block的血統導致它能極為簡單得在不同程式碼作用域之間傳遞上下文。
  2. 相對於NSOperation來說GCD更接近底層,而NSOperation則是更高階抽象,所以GCD在追求效能的底層操作來說,是速度最快的。
  3. GCD需要手動實現非同步操作之間的事務性,順序行,依賴關係等,而NSOperationQueue已經內建了這些支援
  4. 如果非同步操作的過程需要更多的被互動和UI呈現出來,NSOperation會是一個更好的選擇。
  5. 如果需要執行的任務相互的依賴關係較輕,並且需要高併發能力的話,GCD則更有優勢。
  6. 在NSOperationQueue中,我們可以隨時取消已經設定要準備執行的任務(當然,已經開始的任務就無法阻止了),而GCD沒法停止已經加入queue的block(其實是有的,但需要許多複雜的程式碼);
  7. 我們能將KVO應用在NSOperation中,可以監聽一個Operation是否完成或取消,這樣子能比GCD更加有效地掌控我們執行的後臺任務;
  8. 在NSOperation中,我們能夠設定NSOperation的priority優先順序,能夠使同一個並行佇列中的任務區分先後地執行,而在GCD中,我們只能區分不同任務佇列的優先順序,如果要區分block任務的優先順序,也需要大量的複雜程式碼;

總的來說,Operation queue 提供了更多你在編寫多執行緒程式時需要的功能,並隱藏了許多執行緒排程,執行緒取消與執行緒優先順序的複雜程式碼,為我們提供簡單的API入口。從程式設計原則來說,一般我們需要儘可能的使用高等級、封裝完美的API,在必須時才使用底層API。但是我認為當我們的需求能夠以更簡單的底層程式碼完成的時候,簡潔的GCD或許是個更好的選擇,而Operation queue 為我們提供能更多的選擇。

四. GCD常用API的釋義、解析與應用

接下來就是介紹一下GCD常用的API、釋義、解析以及應用

1. 建立和獲取

GCD手動建立佇列部分

// 手動建立佇列API
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
// 建立序列佇列
dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue", DISPATCH_QUEUE_SERIAL);
// 建立並行佇列
dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue", DISPATCH_QUEUE_CONCURRENT);
複製程式碼

手動建立佇列API有兩個可變引數:

  1. const char ***label,為佇列指定一個名稱
  2. dispatch_queue_attr_t attr,關於這個引數官方API有直接的說明:
/*!
* @const DISPATCH_QUEUE_SERIAL
* @discussion A dispatch queue that invokes blocks serially in FIFO order.
*/
#define DISPATCH_QUEUE_SERIAL NULL
/*!
* @const DISPATCH_QUEUE_CONCURRENT
* @discussion A dispatch queue that may invoke blocks concurrently and supports
* barrier blocks submitted with the dispatch barrier API.
*/
#define DISPATCH_QUEUE_CONCURRENT 
      DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, 
      _dispatch_queue_attr_concurrent)
複製程式碼

ISPATCH_QUEUE_SERIAL 建立一個遵循FIFO協議的序列佇列
DISPATCH_QUEUE_CONCURRENT 建立一個併發佇列

序列佇列例項程式碼:

dispatch_queue_t currentQueue = dispatch_queue_create("com.serial.test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(currentQueue, ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"序列佇列非同步事件:%tu ---",i);
        }
    });
    
dispatch_sync(currentQueue, ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"序列佇列同步事件:%tu +++",i);
        }
    });
複製程式碼

並行佇列例項程式碼:

dispatch_queue_t currentQueue = dispatch_queue_create("com.parallel.test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(currentQueue, ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"序列佇列非同步事件:%tu ---",i);
        }
    });
    
dispatch_sync(currentQueue, ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"序列佇列同步事件:%tu +++",i);
        }
    });
複製程式碼

上邊說了一種建立併發佇列的方式,GCD還有另外一種獲取併發佇列的方式,這種方式是蘋果已經為我們寫好的,我們只需要傳入引數就可以了:

dispatch_queue_t concurrent_queue =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
複製程式碼

下邊是官方API:

/*!
 * @function dispatch_get_global_queue
 *
 * @abstract
 * Returns a well-known global concurrent queue of a given quality of service
 * class.
 *
 * @discussion
 * The well-known global concurrent queues may not be modified. Calls to
 * dispatch_suspend(), dispatch_resume(), dispatch_set_context(), etc., will
 * have no effect when used with queues returned by this function.
 *
 * @param identifier
 * A quality of service class defined in qos_class_t or a priority defined in
 * dispatch_queue_priority_t.
 *
 * It is recommended to use quality of service class values to identify the
 * well-known global concurrent queues:
 *  - QOS_CLASS_USER_INTERACTIVE
 *  - QOS_CLASS_USER_INITIATED
 *  - QOS_CLASS_DEFAULT
 *  - QOS_CLASS_UTILITY
 *  - QOS_CLASS_BACKGROUND
 *
 * The global concurrent queues may still be identified by their priority,
 * which map to the following QOS classes:
 *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
 *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
 *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
 *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
 *
 * @param flags
 * Reserved for future use. Passing any value other than zero may result in
 * a NULL return value.
 *
 * @result
 * Returns the requested global queue or NULL if the requested global queue
 * does not exist.
 */
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_CONST DISPATCH_WARN_RESULT DISPATCH_NOTHROW
dispatch_queue_t
dispatch_get_global_queue(long identifier, unsigned long flags);
複製程式碼

可以看到文件對兩個引數都做了解釋,其中第一個引數是標識佇列的優先順序,一共有四種:

#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就OK了。

獲取主佇列

dispatch_queue_t queue = dispatch_get_main_queue();
複製程式碼

2. 同步和非同步

下邊是系統提供的同步和非同步API:

dispatch_sync(dispatch_queue_t  _Nonnull queue, ^(void)block);
dispatch_async(dispatch_queue_t  _Nonnull queue, ^(void)block);
複製程式碼

兩個引數都相同,第一個是需要執行的佇列,第二個是block回撥。

同步函式就是等待任務的執行,等任務執行完成後再返回。所以同步就意味著在當前執行緒中執行任務,並不會開啟新的執行緒,不管是序列佇列還是併發佇列。

dispatch_sync(需要執行的執行緒名稱, ^{ 
      NSLog(@"同步執行緒");
});
複製程式碼

而非同步函式就不會等待任務的執行完成,它會立即返回。所以非同步也就意味著會開啟一個新的執行緒,所以並不會阻塞當前的執行緒。

dispatch_async(需要執行的執行緒名稱, ^{ 
  NSLog(@"非同步執行緒");
});
複製程式碼

3. 佇列組

首先我們看一下佇列組的基礎使用,假設有三個任務,第一個和第二個任務完成之後執行第三個任務:

// 獲取全域性併發佇列
dispatch_queue_t currentGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 獲取主執行緒
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 建立佇列組
dispatch_group_t groupQueue = dispatch_group_create();
// 將第一個任務加入佇列組
dispatch_group_async(groupQueue, currentGlobalQueue, ^{
    NSLog(@"並行任務1");
});
// 將第二個任務加入佇列組
dispatch_group_async(groupQueue, currentGlobalQueue, ^{
    NSLog(@"並行任務2");
});
// 將需要最後執行的任務加入佇列組
dispatch_group_notify(groupQueue, mainQueue, ^{
    NSLog(@"groupQueue中的任務 都執行完成,回到主執行緒更新UI");
});
複製程式碼

佇列組部分API釋義如下
建立佇列組:

dispatch_group_create();
複製程式碼

將任務加入佇列組:

dispatch_group_enter(dispatch_group_t  _Nonnull group);
複製程式碼

參與為需要加入的佇列組

標識加入佇列組的任務已經完成:

dispatch_group_leave(dispatch_group_t  _Nonnull group);
複製程式碼

需要注意的是,enter和leave必須配對。

佇列裡邊所有任務執行完畢之後:

dispatch_group_notify(dispatch_group_t  _Nonnull group, dispatch_queue_t  _Nonnull queue, ^{
        NSLog(@"佇列裡邊所有的任務都執行完畢之後需要執行的事件寫到這");
});
複製程式碼

3. 其他dispatch方法

  • 阻塞當前執行緒dispatch_group_wait:
    dispatch_group_wait ,它會阻塞當前執行緒,直到組裡面所有的任務都完成或者等到某個超時發生。
dispatch_group_wait(dispatch_group_t  _Nonnull group, dispatch_time_t timeout);
複製程式碼

示例程式碼:

dispatch_group_t groupQueue = dispatch_group_create();
dispatch_queue_t conCurrentGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"開始非同步任務");
dispatch_group_async(groupQueue, conCurrentGlobalQueue, ^{
      NSLog(@"等待三秒");
dispatch_group_wait(groupQueue, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
      NSLog(@"執行並行任務");
});
複製程式碼
  • dispatch_after延時新增到佇列
    dispatch_after函式並不會在指定的時間後立即執行任務,時間到後,它只將任務加入到佇列上。因此這段程式碼的作用和你在3秒鐘後用dispatch_async函式將block加到主執行緒上面的作用是一樣的。主執行緒佇列是在RunLoop上執行的,因此,假如RunLoop每1/60秒執行一次任務,那麼上面新增的block會在3秒~3+1/60秒後被執行。如果主執行緒佇列上加了很多工,或者主執行緒延遲了,時間可能更晚。所以,將dispatch_after作為一個精確的定時器使用是有問題的。如果你只是想粗略的延遲一下任務,這個函式還是很有用的。
    函式的第二個引數指定了一個dispatch佇列,用於新增任務。第三個引數,是一個block,即要執行的任務。第一引數指定了延遲的時間,是一個dispatch_time_t型別的引數。這個值可以用dispatch_time函式或dispatch_walltime函式來建立。
    dispatch_time的第一個引數是指定的起始時間,第二個引數是以納秒為單位的一個時間間隔。這個函式以起始時間和時間間隔來建立一個新的時間。如例中所示,通常以DISPATCH_TIME_NOW來作為第一個引數的值,它代表了當前的時間。在下面這段程式碼中,你可以得到一個dispatch_time_t型別的表示1秒鐘之後的時間的變數。
    第二個引數中,NSEC_PER_SEC和數字的乘積會得到一個以納秒為單位的時間間隔值。如果使用NSEC_PER_MSEC,就會得到一個以毫米為單位的時間間隔值。
dispatch_time_t delayTime3 = dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC);
dispatch_time_t delayTime2 = dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
NSLog(@"準備新增執行任務");
dispatch_after(delayTime3, mainQueue, ^{
      NSLog(@"3秒之後新增到佇列");
});
dispatch_after(delayTime2, mainQueue, ^{
      NSLog(@"2秒之後新增到佇列");
});
複製程式碼
  • dispatch_apply多次執行
    dispatch_apply是同步執行的函式,不會立刻返回,在執行完block中的任務後才會返回。功能是把一項任務提交到佇列中多次執行,佇列可以是序列也可以是並行。
    為了不阻塞主執行緒,dispatch_apply正確使用方法是把dispatch_apply放在非同步佇列中呼叫,然後執行完成後通知主執行緒
dispatch_apply(size_t iterations, dispatch_queue_t  _Nonnull queue, ^(size_t)block)
複製程式碼
  • 第一個引數是需要執行的次數
  • 第二個引數是準備提交的佇列
  • 第三個引數是block回撥,裡邊編寫需要被重複執行的程式碼

示例程式碼:

dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
NSLog(@"準備新增執行任務");
dispatch_async(globalQueue, ^{
      dispatch_queue_t applyQueue = dispatch_get_global_queue(0, 0);
      dispatch_apply(3, applyQueue, ^(size_t currentCount) {
            NSLog(@"%tu",currentCount);
      });
      NSLog(@"dispatch_apply 執行完成");
});
複製程式碼
  • dispatch_once 執行一次

    在app執行期間只執行block中的程式碼只執行一次,常用於單例。
    示例程式碼:

    for (int i = 0; i < 10; i ++) {
        NSLog(@"%tu",i);
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"執行一次");
        });
        sleep(1);
    }
    複製程式碼
  • ** dispatch_barrier_async 柵欄**

    在並行佇列中新增多個任務,首先執行dispatch_barrier_async之前新增到佇列裡邊的任務,之後執行dispatch_barrier_async裡的任務,之後再執行dispatch_barrier_async之後新增到佇列裡的任務。必須注意它只對dispatch_queue_create(label, attr)介面建立的併發佇列有作用,如果是Global Queue(dispatch_get_global_queue),這個barrier就不起作用。

示例程式碼:

    dispatch_queue_t conCurrentQueue = dispatch_queue_create("com.multithread.tempApp", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"第一個新增到佇列任務");
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"第二個新增到佇列的任務");
    });
    dispatch_barrier_async(conCurrentQueue, ^{
        NSLog(@"dispatch barrier 阻塞任務");
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"第三個新增到佇列的任務,在barrier之後");
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"最後一個新增到佇列的任務,在barrier之後");
    });
複製程式碼
  • dispatch_set_target_queue 設定優先順序
dispatch_set_target_queue(dispatch_object_t  _Nonnull object, dispatch_queue_t  _Nullable queue)
複製程式碼

dispatch_set_target_queue函式用於設定一個”目標”佇列。這個函式主要用來為新建立的佇列設定優先順序。當用dispatch_queue_create函式建立一個佇列後,無論建立的是並行佇列還是序列佇列,佇列的優先順序都和全域性dispatch佇列的預設優先順序一樣。建立佇列後,你可以用這個函式來修改佇列的優先順序。下面的程式碼演示瞭如何給一個序列佇列設定background優先順序。

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.multithread.tempApp", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);
複製程式碼

在上面程式碼中,dispatch_set_target_queue函式的第一個引數是要設定優先順序的佇列,第二個引數是一個全域性的dispatch佇列,它會被作為目標佇列。這段程式碼會使佇列擁有和目標佇列一樣的優先順序(稍後會解釋這裡的機制)。如果你將主執行緒佇列或者全域性佇列傳遞給dispatch_set_target_queue函式的第一引數,結果不確定,最後不要這麼幹。使用dispatch_set_target_queue函式不僅能夠設定優先順序,也能建立佇列的層次體系。如圖7-8所示,一個序列佇列被設定成了多個序列佇列的目標佇列,在目標佇列上,一次只會有一個佇列被執行。

  • dispatch_semaphore_signal 訊號量
// dispatch_semaphore_signal
    NSString *urlString = [@"https://www.baidu.com" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    // 設定快取策略為每次都從網路載入 超時時間30秒
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 處理完成之後,傳送訊號量
        NSLog(@"正在處理...");
        dispatch_semaphore_signal(semaphore);
    }] resume];
    // 等待網路處理完成
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"處理完成!");
複製程式碼

需要注意的是:在上面的舉例中dispatch_semaphore_signal的呼叫必須是在另一個執行緒呼叫,因為當前執行緒已經dispatch_semaphore_wait阻塞。另外,dispatch_semaphore_wait最好不要在主執行緒呼叫

  • dispatch_suspend(暫停)和dispatch_resume(繼續)
    我們可以使用dispatch_suspend函式暫停一個queue以阻止它執行block物件;使用dispatch_resume函式繼續dispatch queue。呼叫dispatch_suspend會增加queue的引用計數,呼叫dispatch_resume則減少queue的引用計數。當引用計數大於0時,queue就保持掛起狀態。因此你必須對應地呼叫suspend和resume函式。掛起和繼續是非同步的,而且只在執行block之間(比如在執行一個新的block之前或之後)生效。掛起一個queue不會導致正在執行的block停止

五. GCD的一些坑

常用的API和用法都說過了,接下來說一下GCD裡邊常見的坑。

  1. 在主佇列的主執行緒裡邊呼叫主執行緒執行同步任務,會產生死鎖:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue,^{
    NSLog(@"MainQueue");            
});
複製程式碼

執行這段程式碼會發現程式崩潰,block裡邊的MainQueue不會列印出來。
反之用非同步API去執行這段程式碼就沒問題

dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue,^{
  NSLog("MainQueue");            
});
複製程式碼

程式正常執行,block中的程式碼正常執行
這段程式碼崩潰的原因是:主執行緒的獲取和執行都是在主佇列的主執行緒裡邊執行的,然後碰到了需要同步執行的程式碼,主執行緒就會阻塞到同步程式碼的地方,等待同步程式碼執行完畢之後繼續執行。可是由於FIFO協議的存在,序列佇列先進先出,由於主佇列主執行緒的事件是先進去的,所以同步程式碼會等待主佇列主執行緒程式碼執行完畢才會繼續執行。這樣就產生了死鎖,程式崩潰。
如果將這段程式碼放到其他非同步佇列然後由主執行緒執行就不會崩潰了,程式碼如下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        dispatch_sync(mainQueue,^{
            NSLog(@"MainQueue");
        });
    });
複製程式碼

當然必須是非同步佇列,如果是同步佇列的話還是會產生崩潰,原因和剛剛一樣。

  1. 在序列佇列的同步任務裡邊再執行一個同步任務,會發生死鎖:
dispatch_queue_t serial_queue = dispatch_queue_create("com.haley.com", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serial_queue, ^{
        NSLog(@"序列1--同步1");
        dispatch_sync(serial_queue, ^{
            NSLog(@"序列1--同步2");
        });
    });
複製程式碼

程式崩潰,NSLog(@"序列1--同步1");會被執行,NSLog(@"序列1--同步2");不會被執行。崩潰原因和第一條是相同的,嚴格來說第一條只是第二條的一種特殊情況。

  1. 在序列佇列的非同步任務中再巢狀執行同步任務,也會發生死鎖:
dispatch_queue_t serial_queue = dispatch_queue_create("com.haley.com", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serial_queue, ^{
        NSLog(@"序列1--非同步1");
        dispatch_sync(serial_queue, ^{
            NSLog(@"序列1----同步1");
        });
        [NSThread sleepForTimeInterval:2.0];
    });
複製程式碼

NSLog(@"序列1--非同步1");會執行,NSLog(@"序列1----同步1");不會執行。同樣的,由於序列佇列一次只能執行一個任務,任務結束後,才能執行下一個任務。所以非同步任務的結束需要等裡面同步任務結束,而裡面同步任務的開始需要等外面非同步任務結束,所以就相互等待,發生死鎖了。
3. dispatch_apply導致的死鎖

dispatch_queue_t queue = dispatch_queue_create("com.multithread.tempApp", DISPATCH_QUEUE_SERIAL);
dispatch_apply(3, queue, ^(size_t fitstApplyCount) {
    NSLog(@"first apply loop count: %tu", fitstApplyCount);
    //再來一個dispatch_apply!死鎖!
    dispatch_apply(3, queue, ^(size_t secondApplyCount) {
        NSLog(@"second apply loop count %tu", secondApplyCount);
    });
});
複製程式碼

一些需要知道的知識:

  1. 主執行緒序列佇列由系統預設生成的,所以無法呼叫dispatch_resume()和dispatch_suspend()來控制執行繼續或中斷。
  2. 全域性併發佇列由系統預設生成的,所以無法呼叫dispatch_resume()和dispatch_suspend()來控制執行繼續或中斷。

關於GCD的知識先寫到這,其實還有很多東西沒寫,如果有時間再續寫第二篇,不過在這可以想一下下一篇都會涉及到什麼內容:

  1. GCD中不為人所知但是很有用的API
  2. GCD的執行緒安全
  3. 常用API實現原理與原始碼解析

以上,就是GCD的調研結果,並沒有太底層的東西,大部分都是應用層面的,希望以後有時間把缺失的東西補上。

有志者、事竟成,破釜沉舟,百二秦關終屬楚;

苦心人、天不負,臥薪嚐膽,三千越甲可吞吳.

相關文章