GCD 原理詳解

weixin_34185560發表於2018-12-02

原文連結

GCD 簡介

GCD(Grand Central Dispatch)是 Apple 開發的一個多核程式設計的解決方法。它主要用於優化應用程式以支援多核處理器以及其他對稱多處理系統。

GCD 基本概念

GCD 主要包含兩個核心概念:任務佇列

任務

任務:即要線上程中執行的那段程式碼。GCD 將任務定義在 block 中。

任務的執行主要有兩種方式:同步執行(sync)非同步執行(async)。兩者的主要區別是:是否等待佇列中的任務執行結束,是否具備開啟新執行緒的能力。因此,根據任務的執行方式可以將任務分成兩種型別:

同步任務(sync)

  • 同步新增任務到指定的佇列中,在新增的任務執行結束之前,會一直等待,直到佇列裡面的任務完成之後再繼續執行。
  • 只能在當前執行緒中執行任務,不具備開啟新執行緒的能力。

非同步任務(async)

  • 非同步新增任務到指定的佇列中,它不會做任何等待,可以繼續執行任務。
  • 可以在新的執行緒中執行任務,具備開啟新執行緒的能力。

注意: 非同步任務(async) 雖然具有開啟新執行緒的能力,但是並不一定開啟新執行緒。這跟任務所指定的佇列型別有關(下面會講)。

佇列

佇列(Dispatch Queue):即用來存放任務的佇列。佇列是一種特殊的線性表,採用 FIFO(先進先出)的原則,即新任務總是被插入到佇列的末尾,而讀取任務的時候總是從佇列的頭部開始讀取。每讀取一個任務,則從佇列中釋放一個任務。佇列的結構如下圖所示:

2056706-02a4f2a052e23fc5.png
image

在 GCD 中有兩種佇列:序列佇列併發佇列。兩者的主要區別是:執行順序不同,開啟執行緒數不同

序列佇列

  • 每次只有一個任務被執行。(只開啟一個執行緒,一個任務執行完畢後,在執行下一個任務)


    2056706-0f9d5b185e364682.png
    image

併發佇列

  • 允許多個任務(同時)執行。(可以開啟多個執行緒,並同時執行任務)


    2056706-0341d870a2088b3d.png
    image

注意:併發佇列 的併發功能只有在非同步(dispatch_async)函式下才有效。

GCD 使用方法

GCD 的使用主要包含兩個步驟:

  1. 建立一個佇列(序列佇列或併發佇列)
  2. 將任務追加到任務的等待佇列中,然後系統會根據任務型別執行任務(同步執行或非同步執行)

佇列的建立/獲取

dispatch_queue_t
dispatch_queue_create(const char *_Nullable label,
        dispatch_queue_attr_t _Nullable attr)

引數說明:

  • label:表示佇列的唯一識別符號,用於 DEBUG,可為空。
  • attr:表示佇列的型別。DISPATCH_QUEUE_SERIAL 表示序列佇列;DISPATCH_QUEUE_CONCURRENT 表示併發佇列。
// 序列佇列的建立方法
dispatch_queue_t queue = dispatch_queue_create("me.chuquan.testQueue", DISPATCH_QUEUE_SERIAL);
// 併發佇列的建立方法
dispatch_queue_t queue = dispatch_queue_create("me.chuquan.testQueue", DISPATCH_QUEUE_CONCURRENT);

對於序列佇列,GCD 提供了一種特殊的序列佇列:主佇列(Main Dispatch Queue)

  • 所有放在主佇列的任務,都會在主執行緒執行
  • 可使用 dispatch_get_main_queue() 獲取主佇列。
// 主佇列的獲取方法
dispatch_queue_t queue = dispatch_get_main_queue();

對於併發佇列,GCD 預設提供了 全域性併發佇列(Global Dispatch Queue)

  • 可使用 dispatch_get_global_queue 獲取全域性併發佇列。
// 全域性併發佇列的獲取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

GCD 提供了 4 個 全域性併發佇列,分別對應不同的優先順序。

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND:後臺佇列

任務的建立

GCD 提供了同步執行任務的建立方法 dispatch_sync 和非同步執行任務建立方法 dispatch_async

// 同步任務建立方法
dispatch_sync(queue, ^{
    // 這裡放同步執行任務程式碼
});
// 非同步任務建立方法
dispatch_async(queue, ^{
    // 這裡放非同步執行任務程式碼
});

GCD 使用組合

GCD 有兩種佇列(序列佇列/併發佇列),兩種任務(同步任務/非同步任務),可以得到 4 種不同的使用組合。

  1. 同步任務 + 併發佇列
  2. 非同步任務 + 併發佇列
  3. 同步任務 + 序列佇列
  4. 非同步任務 + 序列佇列

實際上,前文還提到兩種特殊的佇列:全域性併發佇列、主佇列。全域性併發佇列可作為普通併發佇列使用。但是主佇列比較特殊,因此又得到 2 種組合:

  1. 同步任務 + 主佇列
  2. 非同步任務 + 主佇列

同步執行 + 併發佇列

/**
 * 同步任務 + 併發佇列
 * 特點:在當前執行緒中執行任務,不會開啟新執行緒,執行完一個任務,再執行下一個任務。
 */
- (void)syncConcurrent {
    NSLog(@"currentThread---%@", [NSThread currentThread]);     // 列印當前執行緒
    NSLog(@"syncConcurrent---begin");

    dispatch_queue_t queue = dispatch_queue_create("me.chuquan.testQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_sync(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"3---%@",[NSThread currentThread]);          // 列印當前執行緒
        }
    });

    NSLog(@"syncConcurrent---end");
}
2056706-a627d690473a6462.png
image

上圖所示為 同步任務 + 併發佇列 的工作原理。

  • syncConcurrent 被新增至主佇列中,在主執行緒執行。
  • 同步任務被新增至並行佇列,並行佇列允許多工同時執行,但由於加入的佇列是同步任務,不會開啟新執行緒,在主執行緒執行。
  • syncConcurrent 會被同步任務阻塞。

執行結果:

currentThread---<NSThread: 0x60000068ee80>{number = 1, name = main}
syncConcurrent---begin
1---<NSThread: 0x60000068ee80>{number = 1, name = main}
1---<NSThread: 0x60000068ee80>{number = 1, name = main}
2---<NSThread: 0x60000068ee80>{number = 1, name = main}
2---<NSThread: 0x60000068ee80>{number = 1, name = main}
3---<NSThread: 0x60000068ee80>{number = 1, name = main}
3---<NSThread: 0x60000068ee80>{number = 1, name = main}
syncConcurrent---end

非同步任務 + 併發佇列

/**
 * 非同步任務 + 併發佇列
 * 特點:可以開啟多個執行緒,任務交替(同時)執行。
 */
- (void)asyncConcurrent {
    NSLog(@"currentThread---%@", [NSThread currentThread]);     // 列印當前執行緒
    NSLog(@"asyncConcurrent---begin");

    dispatch_queue_t queue = dispatch_queue_create("me.chuquan.testQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    dispatch_async(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    dispatch_async(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    NSLog(@"asyncConcurrent---end");
}
2056706-16ba306011fd5df1.png
image

上圖所示為 非同步任務 + 並行佇列 的工作原理。

  • asyncConcurrent 被新增至主佇列中,在主執行緒執行。
  • 非同步任務被新增至並行佇列,並行佇列允許多工同時執行,且非同步任務可以開啟新執行緒,因此每個非同步任務都能啟動一個獨立的執行緒執行。
  • asyncConcurrent 不會被非同步任務阻塞。

執行結果

currentThread---<NSThread: 0x600003e6d580>{number = 1, name = main}
asyncConcurrent---begin
asyncConcurrent---end
1---<NSThread: 0x600003ec2a80>{number = 5, name = (null)}
2---<NSThread: 0x600003ecce40>{number = 3, name = (null)}
3---<NSThread: 0x600003eccec0>{number = 4, name = (null)}
2---<NSThread: 0x600003ecce40>{number = 3, name = (null)}
3---<NSThread: 0x600003eccec0>{number = 4, name = (null)}
1---<NSThread: 0x600003ec2a80>{number = 5, name = (null)}

同步任務 + 序列佇列

/**
 * 同步任務 + 序列佇列
 * 特點:不會開啟新執行緒,在當前執行緒執行任務。任務是序列的,執行完一個任務,再執行下一個任務。
 */
- (void)syncSerial {
    NSLog(@"currentThread---%@", [NSThread currentThread]);     // 列印當前執行緒
    NSLog(@"syncSerial---begin");

    dispatch_queue_t queue = dispatch_queue_create("me.chuquan.testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });
    dispatch_sync(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });
    dispatch_sync(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    NSLog(@"syncSerial---end");
}
2056706-d22910f99383a2a9.png
image

上圖所示為 同步任務 + 序列佇列 的工作原理。

  • syncSerial 被新增至主佇列中,在主執行緒執行。
  • 同步任務被新增至序列佇列,序列佇列不允許多工同時執行,因此同步任務在當前執行緒執行(主執行緒)。
  • syncSerial 會被同步任務阻塞。

非同步任務 + 序列佇列

/**
 * 非同步任務 + 序列佇列
 * 特點:會開啟新執行緒,但是因為任務是序列的,執行完一個任務,再執行下一個任務。
 */
- (void)asyncSerial {
    NSLog(@"currentThread---%@", [NSThread currentThread]);     // 列印當前執行緒
    NSLog(@"asyncSerial---begin");

    dispatch_queue_t queue = dispatch_queue_create("me.chuquan.testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });
    dispatch_async(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });
    dispatch_async(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    NSLog(@"asyncSerial---end");
}
2056706-4ecc793f0996ea3c.png
image

上圖所示為 非同步任務 + 序列佇列 的工作原理。

  • asyncSerial 被新增至主佇列中,在主執行緒執行。
  • 非同步任務被新增至序列佇列,非同步任務能開啟新執行緒,但是序列佇列不允許多工,所以只能開啟一條新執行緒。
  • asyncSerial 不會被非同步任務阻塞。

執行結果

currentThread---<NSThread: 0x600001ef5d00>{number = 1, name = main}
asyncSerial---begin
asyncSerial---end
1---<NSThread: 0x600001e5a740>{number = 3, name = (null)}
1---<NSThread: 0x600001e5a740>{number = 3, name = (null)}
2---<NSThread: 0x600001e5a740>{number = 3, name = (null)}
2---<NSThread: 0x600001e5a740>{number = 3, name = (null)}
3---<NSThread: 0x600001e5a740>{number = 3, name = (null)}
3---<NSThread: 0x600001e5a740>{number = 3, name = (null)}

同步任務 + 主佇列

/**
 * 同步任務 + 主佇列
 * 特點(主執行緒呼叫):互等卡主不執行。
 * 特點(其他執行緒呼叫):不會開啟新執行緒,執行完一個任務,再執行下一個任務。
 */
- (void)syncMain {
    NSLog(@"currentThread---%@", [NSThread currentThread]);     // 列印當前執行緒
    NSLog(@"syncMain---begin");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    NSLog(@"syncMain---end");
}
2056706-2066c39fbd4e59c7.png
image

上圖所示為 同步任務 + 主佇列 的工作原理。

  • syncMain 被新增至主佇列中,在主執行緒執行。
  • 同步任務被新增至主佇列,同步任務不會開啟新執行緒,且主佇列(屬於序列佇列)中的任務只能在主執行緒執行。
  • syncMain 會被同步任務阻塞。但是需要注意的是 syncMain 和同步任務均在主佇列中,同步任務需要等待 syncMain 執行完畢,因此產生死鎖。

執行結果

崩潰

對於這種情況,可以將 syncMain 放置新執行緒執行以避免產生死鎖:

[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];

非同步任務 + 主佇列

/**
 * 非同步任務 + 主佇列
 * 特點:只在主執行緒中執行任務,執行完一個任務,再執行下一個任務
 */
- (void)asyncMain {
    NSLog(@"currentThread---%@", [NSThread currentThread]);     // 列印當前執行緒
    NSLog(@"asyncMain---begin");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    dispatch_async(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    dispatch_async(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]);         // 列印當前執行緒
        }
    });

    NSLog(@"asyncMain---end");
}
2056706-a1ae0e89feddb08f.png
image

上圖所示為 非同步任務 + 主佇列 的工作原理。

  • asyncMain 被新增至主佇列中,在主執行緒執行。
  • 非同步任務被新增至主佇列,非同步任務能開啟新執行緒,但是主佇列(屬於序列佇列)中的任務只能在主執行緒執行。
  • asyncMain 不會被非同步任務阻塞。

執行結果

currentThread---<NSThread: 0x6000014d3700>{number = 1, name = main}
asyncMain---begin
asyncMain---end
1---<NSThread: 0x6000014d3700>{number = 1, name = main}
1---<NSThread: 0x6000014d3700>{number = 1, name = main}
2---<NSThread: 0x6000014d3700>{number = 1, name = main}
2---<NSThread: 0x6000014d3700>{number = 1, name = main}
3---<NSThread: 0x6000014d3700>{number = 1, name = main}
3---<NSThread: 0x6000014d3700>{number = 1, name = main}

GCD 應用

執行緒間通訊

/**
 * 執行緒間通訊
 */
- (void)communication {
    // 獲取全域性併發佇列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 獲取主佇列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        // 非同步追加任務
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]);         // 列印當前執行緒
        }

        // 回到主執行緒
        dispatch_async(mainQueue, ^{
            // 追加在主執行緒中執行的任務
            [NSThread sleepForTimeInterval:2];                  // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]);         // 列印當前執行緒
        });
    });
}
2056706-a854a4002e17cd48.png
image

上圖所示為執行緒間通訊的工作原理。

  • communication 被新增至主佇列中,在主執行緒執行。
  • 非同步任務被新增至全域性佇列(並行佇列),非同步任務能開啟新執行緒,因此非同步任務在新執行緒執行。
  • communication 不會被非同步任務阻塞。
  • 當非同步任務執行完成後,再將一個非同步任務新增至主佇列,主佇列中的任務都在主執行緒執行,所以該非同步任務在主執行緒執行。從而達到執行緒間通訊的目的。

執行結果

1---<NSThread: 0x60000227ec80>{number = 3, name = (null)}
1---<NSThread: 0x60000227ec80>{number = 3, name = (null)}
2---<NSThread: 0x6000022ddd00>{number = 1, name = main}

參考

  1. iOS多執行緒:『GCD』詳盡總結
  2. 細說GCD(Grand Central Dispatch)如何用

相關文章