iOS進階之路 (十六)多線程 - GCD

GCD(Grand Central Dispatch) 是基於C語言的API,是蘋果公司爲多核的並行運算提出的解決方案。GCD會自動利用更多的CPU內核(好比雙核、四核)。程序員只須要將任務添加到隊列中,而且指定執行任務的函數,不須要別寫任何線程管理的代碼。程序員

學習 GCD 以前,先來了解 GCD 中兩個核心概念:任務隊列安全

一. 任務 - Task

任務:就是執行操做的意思,通俗的說就是你在線程中執行的那段代碼。在 GCD 中是放在 block 中的。bash

執行任務有兩種方式:同步執行異步執行。二者的主要區別是:是否等待隊列的任務執行結束,以及是否具有開啓新線程的能力網絡

  1. 同步任務 dispatch_sync
  • 同步添加任務到指定的隊列中,必須等待當前隊列的任務執行完畢,才能執行下一個任務
  • 只能在當前線程中執行任務,不具有開啓新線程的能力。
  1. 異步任務 dispatch_async
  • 異步添加任務到指定的隊列中,它不會作任何等待,能夠繼續執行任務。
  • 能夠在新的線程中執行任務,具有開啓新線程的能力
  • 注意:異步執行雖然具備開啓新線程的能力,可是並不必定開啓新線程。這跟任務所指定的隊列類型有關(下面會講)

二. 隊列 - Dispatch Queue

隊列(Dispatch Queue):這裏的隊列指執行任務的等待隊列,即用來存聽任務的隊列。隊列是一種特殊的線性表,採用 FIFO(First - In -First - Out)的原則,即新任務老是被插入到隊列的末尾,而讀取任務的時候老是從隊列的頭部開始讀取。每讀取一個任務,則從隊列中釋放一個任務。隊列的結構可參考下圖:多線程

在 GCD 中有兩種隊列:串行隊列併發隊列。二者都符合 FIFO 原則。二者的主要區別是:執行順序不一樣,以及開啓線程數不一樣併發

  1. 串行隊列 Serial Dispatch Queue 每次只有一個 任務 被執行,只申請一個 線程 ,一個 任務 執行完畢,再執行下一個

2. 併發隊列 Concurrent Dispatch Queue 能夠申請多個 線程,讓多個 任務 同時執行(注意:併發隊列的併發功能只有在 異步dispatch_async 方法下有效)

三. GCD的使用

GCD使用很簡單,只有三步app

  • 建立 任務(同步任務 或者 異步任務)
  • 建立 隊列(串行隊列 或者 併發隊列)
  • 任務 追加到任務的等待 隊列 中,而後系統就會根據任務類型執行任務(同步執行 或者 異步執行)

3.1 任務的建立

同步執行任務的建立方法 dispatch_sync 和 異步執行任務的建立方法 dispatch_async異步

// 同步任務建立方法
dispatch_sync(queue, ^{
    // 這裏放同步任務代碼
});

// 異步執行任務建立方法
dispatch_async(queue, ^{
    // 這裏放異步任務代碼
});
複製代碼

3.2 隊列的建立

GCD 可使用 dispatch_queue_creat 建立隊列async

dispatch_queue_t queue - dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>);
複製代碼
  • const char * _Nullable label: 隊列的惟一標識符,用於debug,能夠爲空。
  • dispatch_queue_attr_t _Nullable attr: 隊列種類。DISPATCH_QUEUE_SERIAL 串行隊列,DISPATCH_QUEUE_CONCURRENT 併發隊列。

第二個參數若是傳NULL,會建立串行隊列。ide

主隊列

對於串行隊列,GCD默認提供了:主隊列 - Main Dispatch Queue

  • 全部放在主隊列的任務,都會放在主線程執行
  • 可使用 dispatch_get_main_queue() 方法得到主隊列

主隊列本質就是一個普通的串行隊列,只是默認狀況下,代碼放在主隊列中,主隊列的代碼會放到主程序中執行,給咱們主隊列特殊的感受。

全局併發隊列

對於併發隊列,GCD默認提供了:全局併發隊列 - Global Dispatch Queue。可使用 dispatch_get_global_queue() 方法得到全局併發隊列

#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

dispatch_queue_t queue = dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>)
複製代碼
  • long identifier : 標示隊列優先級,通常用 DISPATCH_QUEUE_PRIORITY_DEFAULT
  • 暫時沒用,能夠傳0

4.任務 和 隊列 不一樣組合方式

4.1 同步任務 + 串行隊列

- (void)syncSerial
{
    NSLog(@"主線程 - %@", [NSThread currentThread]);
    NSLog(@"---begin---");
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_sync(queue, ^{
            NSLog(@"同步任務 + 串行隊列:%ld -- %@", (long)i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"---end---");
}
複製代碼

  • 全部任務都在當前線程(主線程)中執行,沒有開啓新的線程。(同步任務只能在當前線程中執行任務,不具有開啓新線程的能力)
  • 全部打印都在 ---begin--- 和 ---end--- 之間執行(同步任務須要等待隊列的任務執行結束)
  • 任務按順序執行(串行隊列每次只有一個任務被執行,任務一個接一個按順序執行)。

4.2 異步任務 + 串行隊列

- (void)asyncSerial
{
    NSLog(@"主線程 - %@", [NSThread currentThread]);
    NSLog(@"---begin---");
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_async(queue, ^{
            NSLog(@"異步任務 + 串行隊列:%ld -- %@", (long)i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"---end---");
}
複製代碼

  • 開啓了一條新線程(異步任務具備開闢線程的能力,串行隊列只開啓一個線程
  • 全部任務在 ---begin--- 和 ---end--- 以後執行(異步執行不會作任何等待,能夠繼續執行接下來的任務
  • 任務按順序執行(串行隊列每次只有一個任務被執行,任務一個接一個按順序執行

4.3 同步任務 + 並行隊列

- (void)syncConcurrent
{
    NSLog(@"主線程 - %@", [NSThread currentThread]);
    NSLog(@"---begin---");
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_sync(queue, ^{
            NSLog(@"同步任務 + 並行隊列:%ld -- %@", (long)i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"---end---");
}
複製代碼

  • 全部任務都在當前線程(主線程)中執行,沒有開闢細膩的線程(同步任務只能在當前線程中執行任務,不具有開啓新線程的能力
  • 全部打印都在 ---begin--- 和 ---end--- 之間執行(同步任務須要等待隊列的任務執行結束)
  • 任務按順序執行:緣由:雖然 併發隊列 能夠開啓多個線程,而且同時執行多個任務。可是由於自己不能建立新線程,只有當前線程這一個線程(同步任務不具有開啓新線程的能力),因此也就不存在併發。並且當前線程只有等待當前隊列中正在執行的任務執行完畢以後,才能繼續接着執行下面的操做(同步任務 須要等待隊列的任務執行結束)。因此任務只能一個接一個按順序執行,不能同時被執行。

4.4 異步任務 + 並行隊列

- (void)asyncConcurrent
{
    NSLog(@"主線程 - %@", [NSThread currentThread]);
    NSLog(@"---begin---");
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_async(queue, ^{
            NSLog(@"異步任務 + 並行隊列:%ld -- %@", (long)i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"---end---");
}
複製代碼

  • 除了當前線程(主線程),系統又開啓了多 個線程,而且任務是交替/同時執行的。(異步任務具有開啓新線程的能力。 併發隊列 可開啓多個線程,同時執行多個任務)。
  • 全部任務在 ---begin--- 和 ---end--- 以後執行(異步執行不會作任何等待,能夠繼續執行接下來的任務

4.5 同步任務 + 主隊列

同步任務 + 主隊列 在不一樣線程中調用結果也是不同,在主線程中調用會發生死鎖問題,而在其餘線程中調用則不會。

  1. 主線程調用 同步任務 + 主隊列

崩潰緣由:主隊列是串行隊列 全部放在主隊列中的任務,都會放到主線程中執行, 當咱們在主線程 中執行syncMain方法,至關於把 syncMain任務 放到了 主線程 的隊列中。而 同步執行會等待當前隊列中的任務執行完畢,纔會接着執行。那麼當咱們把 任務1 追加到主隊列中,任務1 就在等待主線程處理完 syncMain任務。而 syncMain任務 須要等待 任務1 執行完畢,才能接着執行。兩個任務 相互等待 對方執行完畢。

  1. 子線程調用 同步任務 + 主隊列
// 使用 NSThread 的 detachNewThreadSelector 方法會建立線程,並自動啓動線程執行 selector 任務
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
複製代碼

  • 全部任務都是在主線程(非當前線程)中執行的,沒有開啓新的線程(全部放在主隊列中的任務,都會放到主線程中執行) 全部打印都在 ---begin--- 和 ---end--- 之間執行(同步任務須要等待隊列的任務執行結束
  • 任務是按順序執行的(主隊列是 串行隊列,每次只有一個任務被執行,任務一個接一個按順序執行

爲何如今就不會卡住了呢?

由於 syncMain任務 放到 子線程 裏; 而 任務1到10 都在追加到主隊列中,會在 主線程 中執行。主隊列如今沒有正在執行的任務,因此會直接執行主隊列的 任務1 ,等 任務1 執行完畢,再接着執行 任務2。因此這裏不會卡住線程,也就不會形成死鎖問題。

4.6 嵌套使用

如下demo默認都在主隊列,由於主隊列是串行隊列,代碼自上到下依次執行。默認異步操做都是長耗時操做。

  1. 串行異步中嵌套同步
- (void)demo1
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

打印順序:1 5 2 死鎖
複製代碼

  • 代碼塊B爲同步任務,等待,產生阻塞。因此,任務4要等到代碼塊B執行完畢才能執行。
  • 代碼塊B 要執行完畢,必須等到 任務3 執行完畢.
  • 由於是串行隊列,遵循FIFO。任務3 要等到 任務4 執行完畢才執行
  • 因此,任務4塊B塊B任務3任務3任務4。死鎖。
  1. 串行同步中嵌套異步
- (void)demo2
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_sync(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            sleep(2);
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
打印順序: 12453 
複製代碼
  • 主隊列,代碼依次執行:隊列中依次加入任務1 代碼塊A 任務5代碼塊A 是同步任務,等待。 因此打印 1 代碼塊A 5
  • 代碼塊A內: 隊列中依次加入 任務2 代碼塊B 任務4代碼塊B是異步任務,不等待,耗時。因此打印 2 4 3
  • 代碼塊B 執行完畢後 打印5
  • 同步操做保證了 4必定在5以前打印
  1. 並行異步中嵌套同步
- (void)demo3
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
打印結果:15234
複製代碼
  • 主隊列,代碼依次執行:隊列中依次加入任務1 代碼塊A 任務5代碼塊A是異步任務,不等待,耗時。因此打印 1 5
  • 代碼塊A內:並行隊列,申請新線程。新線程依次加入任務2 代碼塊B 任務4代碼塊B同步任務,等待阻塞當前子線程。打印 2 3 4
  • 同步操做保證了 3 必定在 4 以前打印
  1. 並行同步中嵌套異步
- (void)demo4
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_sync(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            sleep(2);
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

打印結果:12453
複製代碼
  • 主隊列,代碼依次執行:隊列中依次加入任務1 代碼塊A 任務5代碼塊A是同步任務,等待。因此打印 1 代碼塊A 5
  • 代碼塊A內:並行隊列,加入 任務2 代碼塊B 任務4代碼塊B異步任務,不等待,耗時,申請新的線程。
  • 同步操做保證 4 必定在 5 以前打印

5. GCD的應用

5.1 dispatch_after 延時執行方法

應用場景:在指定時間以後執行某個任務。

dispatch_after 方法並非在指定時間以後纔開始執行處理,而是在指定時間以後將任務追加到主隊列中。嚴格來講,這個時間並非絕對準確的,但想要大體延遲執行任務,dispatch_after 方法是頗有效的。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"2秒後輸出");
});
複製代碼

5.2 dispatch_once 一次性代碼

應用場景:單例,method-Swizzling

dispatch_once 能保證某段代碼在程序運行過程當中只被執行 1 次,而且即便在多線程的環境下,dispatch_once 也能夠保證線程安全。

- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只執行 1 次的代碼(這裏面默認是線程安全的)
    });
}
複製代碼

5.3 dispatch_apply 快速迭代

應用場景:得到網絡數據後提早算出各個控件的大小

dispatch_apply 按照指定的次數將指定的任務追加到指定的隊列中,並等待所有隊列執行結束。

  • 串行隊列中使用 dispatch_apply,那麼就和 for 循環同樣,按順序同步執行。可是這樣就體現不出快速迭代的意義了。
  • 咱們能夠利用併發隊列進行異步執行。好比說遍歷 0~5 這 6 個數字,for 循環的作法是每次取出一個元素,逐個遍歷。dispatch_apply 能夠 在多個線程中同時(異步)遍歷多個數字。
- (void)apply
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    NSLog(@"dispatch_apply前");
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"dispatch_apply -- 線程%zu-%@", index, [NSThread currentThread]);
    });
    NSLog(@"dispatch_apply後");
}
複製代碼

不管是在串行隊列,仍是併發隊列中,dispatch_apply 都會等待所有任務執行完畢, ,這點就像是 同步操做,也像是隊列組中的 dispatch_group_wait方法。

5.4 dispatch_barrier 柵欄函數

應用場景:線程同步

異步任務+併發隊列會開闢線程,多個任務同時進行,各任務也會由於任務複雜度和cpu的調度致使執行完畢亂序。如何設置讓任務順序執行完畢,柵欄函數就是很好的解決辦法。

  • dispatch_barrier_async 用於提交異步執行柵欄函數塊任務,並當即返回。該函數不會阻塞線程,只會阻塞任務執行。柵欄函數提交以後並不會當即執行,而是會直接返回。等待在柵欄函數以前提交的任務都執行完成以後,柵欄函數任務會自動執行,在柵欄函數以後提交的任務必須在柵欄函數執行完成以後纔會執行.

  • dispatch_barrier_sync 用於提交同步執行柵欄函數塊任務,並不會當即返回,須要等待柵欄函數提交的任務執行完畢以後纔會返回。與dispatch_barrier_async不一樣,因爲該函數須要同步執行,在該柵欄函數任務執行完成以前,函數不會返回,因此此時後邊的任務都會被阻塞.

  1. 測試1:
- (void)barrierSync
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"任務1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_barrier_sync(queue, ^{
        NSLog(@"dispatch_barrier_sync -- %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"任務2 -- %@", [NSThread currentThread]);
    });
    
    NSLog(@"end -- %@", [NSThread currentThread]);
}
複製代碼

  1. 測試2
- (void)barrierAsync
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"任務1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"dispatch_barrier_async -- %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"任務2 -- %@", [NSThread currentThread]);
    });
    
    NSLog(@"end -- %@", [NSThread currentThread]);
}
複製代碼

比較 測試1測試2

  1. dispatch_barrier_syncdispatch_barrier_async 的共同點:
  • 都會等待在它前面插入隊列的任務(任務begin 任務1)先執行完。
  • 都會等待他們本身的任務(barrier operation)執行完以後再執行後面的任務(任務2 任務end)。
  1. dispatch_barrier_syncdispatch_barrier_async 的不一樣點:
  • 將任務插入到隊列時,dispatch_barrier_sync 須要等待本身的任務(barrier operation)執行完畢後,纔會插入後面的任務(任務2 任務end),而後執行後面的任務。因此 任務endbarrier operation 後打印。說白了,會阻塞線程。
  • dispatch_barrier_async 將本身的任務插入到隊列以後,不會等待本身的任務(barrier operation)執行結果,它會繼續插入後面的任務。因此 任務end 先打印。

  1. 測試3:dispatch_barrier_sync線程任務線程 不一致,會怎樣?
- (void)barrierSync
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    // 新建線程
    dispatch_queue_t queue2 = dispatch_queue_create("com.akironer2", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"任務1 -- %@", [NSThread currentThread]);
    });
    
    // 阻塞新建線程
    dispatch_barrier_sync(queue2, ^{
        NSLog(@"dispatch_barrier_sync -- %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"任務2 -- %@", [NSThread currentThread]);
    });
    
    NSLog(@"end -- %@", [NSThread currentThread]);
}
複製代碼

使用 dispatch_barrier_async 測試,也有一樣的結果

  • barrier的本質是阻塞線程。dispatch_barrier 線程任務線程 不一致,barrier沒有發揮做用。
  • 因此咱們使用AFNetworking作網絡請求時,不能用柵欄函數起到同步鎖堵塞的效果,由於AFNetworking內部有本身的隊列。
  1. 測試4:對全局併發隊列使用柵欄函數,有什麼效果?

  • 對全局併發隊列使用柵欄函數, 可能導致系統其餘調用全局隊列的地方也堵塞從而致使崩潰。

5.5 dispatch_group 調度組

應用場景:分別異步執行2個耗時任務,而後當2個耗時任務都執行完畢後再回到主線程執行任務。

5.5.1 dispatch_group_async & dispatch_group_notify

  • dispatch_group_async 先把任務放到隊列中,而後將隊列放入隊列組中
  • dispatch_group_notify 監聽 group 中任務的完成狀態,當全部的任務都執行完成後,追加任務group 中,並執行任務。
- (void)groupAsync
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    dispatch_group_async(group, queue, ^{
        sleep(5);
        NSLog(@"任務1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"任務2 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(4);
        NSLog(@"任務3 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"end -- %@", [NSThread currentThread]);
    });
}

打印:
22:23:12.180761+0800 GCD[2123:330417] begin -- <NSThread: 0x60000212ad40>{number = 1, name = main}
22:23:15.182107+0800 GCD[2123:330584] 任務2 -- <NSThread: 0x600002150080>{number = 3, name = (null)}
22:23:16.180985+0800 GCD[2123:330585] 任務3 -- <NSThread: 0x60000215c180>{number = 4, name = (null)}
22:23:17.182406+0800 GCD[2123:330596] 任務1 -- <NSThread: 0x600002122ec0>{number = 5, name = (null)}
22:23:17.182667+0800 GCD[2123:330596] end -- <NSThread: 0x600002122ec0>{number = 5, name = (null)}
複製代碼

5.5.2 dispatch_group_wait

暫停當前線程,等待指定的group中的任務執行完成以後,才往下執行任務。相比於dispatch_group_notify, dispatch_group_wait 會阻塞線程。

- (void)groupWait
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_group_async(group, queue, ^{
        sleep(5);
        NSLog(@"任務1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"任務2 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(4);
        NSLog(@"任務3 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    NSLog(@"end -- %@", [NSThread currentThread]);
}

打印:
22:31:45.390633+0800 GCD[2157:336097] begin -- <NSThread: 0x6000001d6d00>{number = 1, name = main}
22:31:48.392504+0800 GCD[2157:336257] 任務2 -- <NSThread: 0x6000001a4640>{number = 6, name = (null)}
22:31:49.392455+0800 GCD[2157:336255] 任務3 -- <NSThread: 0x6000001dc400>{number = 5, name = (null)}
22:31:50.395577+0800 GCD[2157:336256] 任務1 -- <NSThread: 0x6000001ac380>{number = 4, name = (null)}
22:31:50.395978+0800 GCD[2157:336256] end -- <NSThread: 0x6000001ac380>{number = 4, name = (null)}
複製代碼

5.5.3 dispatch_group_enter & dispatch_group_leave

  • dispatch_group_enter 標誌着一個任務追加到 group,執行一次,至關於 group 中未執行完畢任務數 +1
  • dispatch_group_leave 標誌着一個任務離開了 group,執行一次,至關於 group 中未執行完畢任務數 -1。
  • 當 group 中未執行完畢任務數爲0的時候,纔會使 dispatch_group_wait 解除阻塞,以及執行追加到 dispatch_group_notify 中的任務。
- (void)groupWait
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        sleep(5);
        NSLog(@"任務1 -- %@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"任務2 -- %@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"end -- %@", [NSThread currentThread]);
    });
}

打印:
22:42:10.338809+0800 GCD[2188:343124] begin -- <NSThread: 0x60000338adc0>{number = 1, name = main}
22:42:13.340545+0800 GCD[2188:343288] 任務2 -- <NSThread: 0x6000033e8340>{number = 5, name = (null)}
22:42:15.343788+0800 GCD[2188:343293] 任務1 -- <NSThread: 0x600003304000>{number = 7, name = (null)}
22:42:15.344152+0800 GCD[2188:343293] end -- <NSThread: 0x600003304000>{number = 7, name = (null)}
複製代碼

5.6 dispatch_semaphore 信號量

應用場景

  • 保持線程同步,將異步執行任務轉化爲同步執行任務
  • 保證線程安全,爲線程加鎖。

信號量是基於計數器的一種多線程同步機制,用來管理對資源的併發訪問。信號量內部有一個能夠原子遞增或遞減的值。若是一個動做嘗試減小信號量的值,使其小於0,那麼這個動做將會被阻塞,直到有其餘調用者(在其餘線程中)增長該信號量的值。

  • dispatch_semaphore_create:建立一個 Semaphore 並初始化信號的總量
  • dispatch_semaphore_signal:發送一個信號,讓信號總量加 1
  • dispatch_semaphore_wait:可使總信號量減 1。信號量小於等於 0 則會阻塞當前線程,直到信號量大於0或者通過輸入的時間值;若信號量大於0,則會使信號量減1並返回,程序繼續住下執行。

dispatch_semaphore_waitdispatch_semaphore_signal 這兩個函數中間的執行代碼,每次只會容許限定數量的線程進入,這樣就有效的保證了在多線程環境下,只能有限定數量的線程進入。

注意:信號量的使用前提是:想清楚你須要處理哪一個線程等待(阻塞),又要哪一個線程繼續執行,而後使用信號量。

5.6.1 dispatch_semaphore 線程同步

- (void)semaphoreAync
{
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d -- 線程%@", i, [NSThread currentThread]);
            // 打印任務結束後信號量解鎖
            dispatch_semaphore_signal(sem);
        });
        // 異步耗時,因此這裏信號量加鎖
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    }
}
複製代碼

  • semaphore 初始建立時計數爲 0。
  • 異步執行 將 任務1 追加到隊列以後,不作等待,接着執行 dispatch_semaphore_wait ,semaphore 減 1,此時 semaphore == -1,當前線程進入阻塞狀態。
  • 任務1 開始執行。執行到 dispatch_semaphore_signal 以後,總信號量加 1,此時 semaphore == 0,正在被阻塞的線程恢復繼續執行。

5.6.2 dispatch_semaphore 線程安全

- (void)saleTickets
{
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    self.ticketCount = 20;
    
    // queue1 表明售賣窗口1
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 表明售賣窗口2
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketsSemaphore];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketsSemaphore];
    });
}

- (void)saleTicketsSemaphore
{
    while (1) {
        // 至關於加鎖
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        if (self.ticketCount > 0) {
            self.ticketCount --;
            NSLog(@"剩餘票數:%ld 窗口:%@", (long)self.ticketCount, [NSThread currentThread]);
            sleep(0.1);
        } else {
            NSLog(@"賣完了%@", [NSThread currentThread]);
            // 至關於解鎖
            dispatch_semaphore_signal(self.semaphore);
            break;
        }
        // 至關於解鎖
        dispatch_semaphore_signal(self.semaphore);
    }
}
複製代碼

5.6.3 dispatch_semaphore 最大併發量

- (void)dispatchAsyncLimit
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    
    //任務1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任務1執行--%@", [NSThread currentThread]);
        sleep(1);
        NSLog(@"任務1完成--%@", [NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
    
    //任務2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任務2執行--%@", [NSThread currentThread]);
        sleep(1);
        NSLog(@"任務2完成--%@", [NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
    
    //任務3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任務3執行--%@", [NSThread currentThread]);
        sleep(1);
        NSLog(@"任務3完成--%@", [NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
}
![](https://user-gold-cdn.xitu.io/2020/4/26/171b23c361a2d077?w=2136&h=224&f=png&s=203093)
複製代碼

因爲設定的信號值爲2,先執行2個線程,等執行完一個,纔會繼續執行下一個,保證同一時間執行的線程數不超過2

5.7 dispatch_source

應用場景:GCDTimer

NSTimer是依賴Runloop的,而Runloop能夠運行在不一樣的模式下。若是Runloop在阻塞狀態,NSTimer觸發時間就會推遲到下一個Runloop週期,所以NSTimer在計時上會有偏差。而GCD定時器不依賴Runloop,計時精度要高不少。

// 強持有
@property (nonatomic, strong) dispatch_source_t timer;

// 1.建立隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2.建立timer
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 3.設置timer首次執行時間,間隔,精確度
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
// 4.設置timer事件回調
dispatch_source_set_event_handler(_timer, ^{
    NSLog(@"GCDTimer");
});
// 5.默認是掛起狀態,須要手動激活
dispatch_resume(_timer);
複製代碼

參考資料

官方文檔 -- Dispatch

滿庭花醉三千客 —— 柵欄函數

相關文章
相關標籤/搜索