GCD的使用和原理

在我們做iOS開發(fā)的過程中,經(jīng)常會與多線程打交道,異步繪制,網(wǎng)絡(luò)請求等,方式有NSThread,NSOperationQueue,GCD等,在這里GCD的地位舉足輕重,那么今天寫一篇關(guān)于GCD的文章。首先迎來第一個問題:

什么是GCD
全名叫 Grand Central Dispatch 是一種異步執(zhí)行任務(wù)的技術(shù),一套基于c語言實現(xiàn)的api,語法十分簡潔,只需要簡單定義任務(wù)或按需加入到隊列中,就可以按照計劃實現(xiàn)功能,下面一個簡單的例子。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //網(wǎng)絡(luò)請求
        //異步繪制圖像
        //數(shù)據(jù)庫訪問等
        dispatch_async(dispatch_get_main_queue(), ^{
            //刷新UI
        });
    });

在這里使用異步的方式定義了一個任務(wù),并將其添加到一個全局隊列中,這里面的任務(wù)內(nèi)容可以進行一些耗時操作,由于是異步所以不影響主線程,當(dāng)任務(wù)結(jié)束之后,同樣使用異步的方式定義了一個任務(wù),將其添加到了主隊列中,這里執(zhí)行的任務(wù)會在主線程完成。

同步和異步
前面內(nèi)容提到了“異步”一詞,那么這里聊一下什么是“異步”,相應(yīng)的還有“同步”一詞。
異步的作用是不需要CPU立刻響應(yīng),而是等待一個信號或回調(diào)來下一步操作,當(dāng)前線程可以立刻執(zhí)行后續(xù)內(nèi)容,而同步為可以阻塞當(dāng)前線程,當(dāng)執(zhí)行完一個任務(wù)之后才能執(zhí)行下一個。
異步是目的,多線程是手段
當(dāng)我們開啟一個新的線程之后,可以將任務(wù)交由新的線程執(zhí)行,實現(xiàn)異步效果,當(dāng)需要的時候再返回之前的線程。

用圖來做一個同步和異步的解釋


異步和同步

這里thread1模擬主線程,當(dāng)執(zhí)行任務(wù)A的時候由于耗時比較短,所以可以在主線程上完成,當(dāng)執(zhí)行到任務(wù)B的時候,由于耗時長,所以開辟了一條新的線程,將任務(wù)交由子線程處理,同時繼續(xù)完成任務(wù)C,當(dāng)耗時操作完成之后,將結(jié)果返回給主線程進行操作,實現(xiàn)了異步功能。

使用多線程仍然要注意幾個問題,1.數(shù)據(jù)競爭 2.死鎖 3.線程開辟太多導(dǎo)致內(nèi)存消耗過大,不過只要使用得當(dāng),多線程對我們開發(fā)的好處十分巨大。

串行隊列和并發(fā)隊列
首先解釋一下“隊列”,如其名字,它是執(zhí)行處理的等待隊列,這里的隊列調(diào)度方式為先進先出模式(FIFO-First In First Out),也就是先添加到隊列中的任務(wù)先執(zhí)行,當(dāng)然還有其他幾種模式,不過這里暫時不考慮,有興趣的請自行查資料

FIFO

隊列的種類也是兩種,Serial Dispatch Queue和 Concurrent Dispatch Queue,分別為串行隊列和并發(fā)隊列,串行隊列中的任務(wù)必須按照順序依次執(zhí)行,并發(fā)隊列可以多個線程以并發(fā)的形式執(zhí)行。
串行隊列和并發(fā)隊列

下面用代碼來看串行隊列和并發(fā)隊列的區(qū)別,首先使用串行隊列的方式創(chuàng)建幾個異步操作

dispatch_queue_t queue = dispatch_queue_create("com.example.gcdDemo", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"%@---0", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"%@---1", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"%@---2", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"%@---3", [NSThread currentThread]);
    });

看看輸出結(jié)果

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

很明顯,這里是同一個線程,而且也確實是按照順序執(zhí)行的,那么接下來使用并發(fā)隊列來操作。

dispatch_queue_t queue = dispatch_queue_create("com.example.gcdDemo", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"%@---0", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"%@---1", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"%@---2", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"%@---3", [NSThread currentThread]);
    });

再看看輸出結(jié)果

<NSThread: 0x60400046adc0>{number = 5, name = (null)}---2
<NSThread: 0x60400046b000>{number = 3, name = (null)}---0
<NSThread: 0x6000004655c0>{number = 4, name = (null)}---1
<NSThread: 0x6000004653c0>{number = 6, name = (null)}---3

很明顯,并沒有按照順序執(zhí)行,而且不是同一個線程,這就印證了上面所說的串行隊列和并發(fā)隊列的區(qū)別。
但是不代表創(chuàng)建了多少個并發(fā)任務(wù)就會開辟多少個線程,這個線程數(shù)量由XNU內(nèi)核來決定,將上面的案例調(diào)整為8個任務(wù),可以看到有些線程是重復(fù)的,表明了如果一開始就可以開辟8個線程,就不會出現(xiàn)這種情況,所以當(dāng)一個任務(wù)結(jié)束之后,下一個任務(wù)可以放到完成的線程中,但是和串行隊列還是有區(qū)別的


多個任務(wù)的并發(fā)隊列

首先會開辟4條線程處理任務(wù),將task0-task3異步形式放入線程中執(zhí)行,假定task0首先完成,并發(fā)隊列中取出task4放入線程3中執(zhí)行,以此類推,所以也就造成了有些線程是重復(fù)的原因。

DISPATCH MAIN QUEUE/DISPATCH GLOBAL QUEUE
我們平時常用的大多是這兩種,前者是在主線程執(zhí)行的queue,由于主線程只有一個,自然是個串行隊列,而global_queue是個并發(fā)隊列,這個隊列有4個優(yōu)先級,分別是低優(yōu)先級(DISPATCH_QUEUE_PRIORITY_LOW),默認(rèn)優(yōu)先級(DISPATCH_QUEUE_PRIORITY_DEFAULT),高優(yōu)先級(DISPATCH_QUEUE_PRIORITY_HIGH),和后臺優(yōu)先級(
DISPATCH_QUEUE_PRIORITY_BACKGROUND),代碼如下

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

GCD的api

前面已經(jīng)提到一些關(guān)于GCD的api,下面會詳細的探討這些api的功能
dispatch_queue_create

dispatch_queue_t serialDispatchQueue = dispatch_queue_create("com.example.gcdDeo",NULL);

這里是創(chuàng)建一個queue的方法,返回類型為dispatch_queue_t,方法含有兩個參數(shù),一個是隊列名,推薦使用應(yīng)用程序ID逆序的域名方式,后面參數(shù)表示生成的隊列類型,一種為DISPATCH_QUEUE_SERIAL為串行隊列,這里也可以使用NULL,這個宏定義的值就是NULL,另一種是DISPATCH_QUEUE_CONCURRENT表示并發(fā)隊列,這里雖然串行隊列中的任務(wù)是按順序執(zhí)行的,但是如果創(chuàng)建多個串行隊列,是會并行處理。


并行執(zhí)行的多個Serial Dispatch Queue

由于一個隊列只操作一個線程,所以在執(zhí)行一些比較重要的操作時,盡量使用串行隊列,避免造成數(shù)據(jù)沖突。

dispatch_(a)sync
最常用的就是這兩個函數(shù)了dispatch_sync同步執(zhí)行,dispatch_async異步執(zhí)行,第一個參數(shù)為指定的隊列名,block為執(zhí)行的任務(wù)內(nèi)容,但是使用gcd也要注意一些問題

    NSLog(@"1");
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    
    NSLog(@"3");
 1
//接下來報錯
同步主隊列的問題

看得出這個問題的原因,同步的方式在主線程執(zhí)行一個任務(wù),造成了線程阻塞。

dispatch_after
當(dāng)我們需要在一段時間之后延遲執(zhí)行某些任務(wù)的時候,可以使用dispatch_after這個函數(shù),示例代碼如下:

NSLog(@"延遲之前");
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
    NSLog(@"延遲3秒執(zhí)行");
});

這里的返回結(jié)果為

2018-09-04 14:52:59.437518+0800 GCDDemo[75192:21501573] 延遲之前
2018-09-04 14:53:02.438042+0800 GCDDemo[75192:21501573] 延遲3秒執(zhí)行

可以看得出的確是延遲了3秒執(zhí)行的任務(wù),不過這里的實際意義為在3秒后將任務(wù)添加到主隊列中執(zhí)行,由于Main Dispatch Queue在主線程的Runloop中,所以真實的時間為3秒加上最短立刻最長Runloop一次循環(huán)的時間,并且如果Runloop中堆積的任務(wù)較多,可能時間會更長,所以時間并不是完全精確,不過想要大致完成延遲功能,dispatch_after是完全沒有問題的。
time為dispatch_time_t類型,從第一個參數(shù)時間開始,到第二個指定后的時間結(jié)束,這里NSEC_PER_SEC是時間類型,以秒為單位,NSEC_PER_MSEC是以毫秒做單位,其他類型單位這里不多解釋了。

dispatch_group
項目中我們經(jīng)常會遇到將多個異步任務(wù)的結(jié)果同時返回,如果只是同步的話,執(zhí)行完最后一個任務(wù)就是結(jié)束,但是異步卻不行,所以這里我們需要使用dispatch_group,代碼如下

    NSLog(@"全部開始-----%@", [NSThread currentThread]);
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_async(group, queue, ^{
        sleep(4);
        NSLog(@"子線程1-----%@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"子線程2-----%@", [NSThread currentThread]);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"全部結(jié)束-----%@", [NSThread currentThread]);
    });

這里使用sleep模擬了兩個耗時操作,并且打印了幾個位置所處的線程,首先使用dispatch_group_create創(chuàng)建了一個組,并且使用了global queue,接下來模擬異步延時操作,使用dispatch_group_async這個函數(shù)和dispatch_async的功能是一樣的,只不過前者歸屬于第一個參數(shù)這個組,當(dāng)函數(shù)全部結(jié)束之后會調(diào)用dispatch_group_notify這個方法,表示所有異步操作都已經(jīng)結(jié)束,代碼執(zhí)行結(jié)果如下

2018-09-04 16:22:03.449143+0800 GCDDemo[76086:21787405] 全部開始-----<NSThread: 0x6040002601c0>{number = 1, name = main}
2018-09-04 16:22:06.451174+0800 GCDDemo[76086:21787469] 子線程2-----<NSThread: 0x60400027ed40>{number = 3, name = (null)}
2018-09-04 16:22:07.452564+0800 GCDDemo[76086:21787470] 子線程1-----<NSThread: 0x600000470100>{number = 4, name = (null)}
2018-09-04 16:22:07.452926+0800 GCDDemo[76086:21787405] 全部結(jié)束-----<NSThread: 0x6040002601c0>{number = 1, name = main}

這里可以看出的確是當(dāng)兩個子線程都完成之后,才回到主線程的回調(diào)中,另外一種方式通過dispatch_group_wait方式阻止后續(xù)操作,直到異步函數(shù)全部完成

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"全部結(jié)束-----%@", [NSThread currentThread]);

這里wait表示等待,第一個參數(shù)表示等待的對象,類型是dispatch_group_t,第二個參數(shù)表示等待時間,這里使用DISPATCH_TIME_FOREVER表示一直等待,所以這個函數(shù)變成了直到組中代碼全部結(jié)束,等待才會停止,不過這里還是更加推薦使用dispatch_group_notify方式,因為dispatch_group_wait是同步的,所以不推薦在主線程中使用。

dispatch_group_enter/dispatch_group_leave
實際工作中,會出現(xiàn)讓多個請求同時返回結(jié)果的案例,這時如果用上面的方法會造成一定問題,因為上面異步的任務(wù)只是一個NSLog,如果是一個延時請求呢,下面模擬一下多個請求同時發(fā)生的案例。

NSLog(@"全部開始-----%@", [NSThread currentThread]);
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.example.gcdDemo", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(4);
            NSLog(@"模擬請求1-----%@", [NSThread currentThread]);
        });
    });
    
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(3);
            NSLog(@"模擬請求2-----%@", [NSThread currentThread]);
        });
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"全部結(jié)束-----%@", [NSThread currentThread]);
    });

這里在dispatch_group_async這些個異步任務(wù)中,再次用異步的方式模擬了一個請求任務(wù),下面看一下結(jié)果

2018-09-04 16:50:50.221045+0800 GCDDemo[76421:21817204] 全部開始-----<NSThread: 0x60000007ecc0>{number = 1, name = main}
2018-09-04 16:50:50.249321+0800 GCDDemo[76421:21817204] 全部結(jié)束-----<NSThread: 0x60000007ecc0>{number = 1, name = main}
2018-09-04 16:50:53.225810+0800 GCDDemo[76421:21817394] 模擬請求2-----<NSThread: 0x604000663000>{number = 5, name = (null)}
2018-09-04 16:50:54.225917+0800 GCDDemo[76421:21817393] 模擬請求1-----<NSThread: 0x6040004649c0>{number = 3, name = (null)}

造成這樣的原因是發(fā)起請求的兩個任務(wù)已經(jīng)完成了,所以調(diào)用了notify方法,但是請求的結(jié)果還沒有成功,所以這樣的代碼是有問題的,那么這里就使用dispatch_group_enter和dispatch_group_leave這對方法來解決。

NSLog(@"全部開始-----%@", [NSThread currentThread]);
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.example.gcdDemo", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_async(group, queue, ^{
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(4);
            NSLog(@"模擬請求1-----%@", [NSThread currentThread]);
            dispatch_group_leave(group);
        });
    });
    
    dispatch_group_async(group, queue, ^{
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(3);
            NSLog(@"模擬請求2-----%@", [NSThread currentThread]);
            dispatch_group_leave(group);
        });
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"全部結(jié)束-----%@", [NSThread currentThread]);
    });

這樣的結(jié)果就沒有問題了,這對方法可以理解為retain和release,當(dāng)每次進入到一個新的異步任務(wù)中時,使用dispatch_group_enter告訴原本的異步任務(wù)這里還沒有結(jié)束,當(dāng)請求完成之后使用dispatch_group_leave表示已經(jīng)結(jié)束了,可以退出這個異步操作了,這樣的話,才能保證真正完成了一個延時函數(shù),結(jié)果如下

2018-09-04 16:54:22.832901+0800 GCDDemo[76462:21821447] 全部開始-----<NSThread: 0x60000007e980>{number = 1, name = main}
2018-09-04 16:54:25.837875+0800 GCDDemo[76462:21821502] 模擬請求2-----<NSThread: 0x604000460840>{number = 3, name = (null)}
2018-09-04 16:54:26.833634+0800 GCDDemo[76462:21821503] 模擬請求1-----<NSThread: 0x6000004676c0>{number = 4, name = (null)}
2018-09-04 16:54:26.833992+0800 GCDDemo[76462:21821447] 全部結(jié)束-----<NSThread: 0x60000007e980>{number = 1, name = main}

當(dāng)前案例還有其他的解決方案,我準(zhǔn)備在下一篇文章中將案例的其他解決辦法寫出來,這里不加贅述了。

dispatch_barrier_(a)sync
再次通過一個案例來認(rèn)識這個函數(shù),假設(shè)有a,b,c,d四個異步任務(wù),我新增加了一個任務(wù)new,我希望可以在a和b之后完成并且在c和d之前完成,這里可以通過同步等其他方式解決,不過有個更好的方式解決這個問題,dispatch_barrier_async,和它的名字一樣,可以理解為一個柵欄,將前后分隔開,在下先上代碼

dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        sleep(3);
        NSLog(@"task - A");
    });
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"task - B");
    });
    
    NSLog(@"before barrier");
    
    dispatch_barrier_sync(queue, ^{
        NSLog(@"task - new");
    });
    
    NSLog(@"after barrier");
    
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"task - C");
    });
    
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"task - D");
    });

這里模擬了四個異步操作任務(wù),將中間位置插入了一個new任務(wù),這里看看返回結(jié)果

2018-09-04 17:55:08.783989+0800 GCDDemo[76920:21886183] before barrier
2018-09-04 17:55:10.787647+0800 GCDDemo[76920:21886310] task - B
2018-09-04 17:55:11.785806+0800 GCDDemo[76920:21886309] task - A
2018-09-04 17:55:11.786003+0800 GCDDemo[76920:21886183] task - new
2018-09-04 17:55:11.786164+0800 GCDDemo[76920:21886183] after barrier
2018-09-04 17:55:12.790452+0800 GCDDemo[76920:21886312] task - D
2018-09-04 17:55:13.790432+0800 GCDDemo[76920:21886309] task - C

這里可以看出,由于有模擬延時操作,所以before首先輸出,然后執(zhí)行了A和B兩個任務(wù),而barrier中的任務(wù)并沒有延時卻在A和B之后,說明柵欄功能生效,有因為是sync同步的關(guān)系,所以后面的after緊接著輸出,這里把sync換成async會有什么結(jié)果呢

2018-09-04 17:59:29.114406+0800 BlockDemo[76948:21890748] before barrier
2018-09-04 17:59:29.114600+0800 BlockDemo[76948:21890748] after barrier

before和after是緊挨著輸出的,說明dispatch_barrier_sync同樣有同步的作用,而dispatch_barrier_async也有著異步的效果。

dispatch_apply
如果有一個案例需要按指定次數(shù)執(zhí)行g(shù)cd的任務(wù),可以用這個函數(shù),第一個參數(shù)為次數(shù),第二個參數(shù)為指定的隊列,第三個參數(shù)是帶參數(shù)的block,參數(shù)為當(dāng)前次數(shù)下標(biāo)

NSArray * array = @[@"1",@"2",@"3",@"4",@"5",];
    dispatch_apply([array count], dispatch_get_global_queue(0, 0), ^(size_t index) {
        NSLog(@"元素:%@----第%ld次", array[index],index);
    });
2018-09-04 18:44:56.970194+0800 GCDDemo[77406:21942336] 元素:4----第3次
2018-09-04 18:44:56.970194+0800 GCDDemo[77406:21942205] 元素:1----第0次
2018-09-04 18:44:56.970194+0800 GCDDemo[77406:21942337] 元素:3----第2次
2018-09-04 18:44:56.970194+0800 GCDDemo[77406:21942335] 元素:2----第1次
2018-09-04 18:44:56.970380+0800 GCDDemo[77406:21942205] 元素:5----第4次

dispatch_semaphore
前文已經(jīng)有講部分關(guān)于產(chǎn)生不一樣數(shù)據(jù)的處理問題,不過有時需要更細致的排他控制,例如你到了一個屋子,里面有一個椅子,你可以坐下,如果再拿來一個椅子,依然可以坐下,但是如果把椅子拿走,那么只能站著等待了,semaphore使用信號量的方式實現(xiàn)了類似的情況,首先第一個函數(shù):

dispatch_semaphore_create(0);

這里創(chuàng)建了一個semaphore,返回類型是dispatch_semaphore_t,有一個參數(shù),表示初始信號量的值,這里給0,如果遇到wait則需要等待。

dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(semaphore);

當(dāng)?shù)谝粋€函數(shù)中的信號量0時,這個函數(shù)需要進行等待,如果大于0,則將信號量-1,第二個參數(shù)為等待的時間,這個可以根據(jù)需求使用,這里假定需要等待無限長的時間,知道信號量增加未知
第二個函數(shù)中會給信號量+1,可以看出這兩個函數(shù)是要成對出現(xiàn)的,如果單獨出現(xiàn)wait而且初始化為0的話,會造成后續(xù)任務(wù)全部卡住無法執(zhí)行。下面給一個小的案例

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(3);
            NSLog(@"完成1");
            dispatch_semaphore_signal(semaphore);
        });
    });
    
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(2);
            NSLog(@"完成2");
            dispatch_semaphore_signal(semaphore);
        });
    });
    
    
    dispatch_group_notify(group, queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"全部完成");
    });

這里再次模擬了多個請求需要同時返回結(jié)果的問題,通過信號量的方式,使用信號量的方式同樣可以完成這個需求,當(dāng)notify中有兩個等待信號的時候,只能通過請求成功的信號量增加的方法去抵消,當(dāng)兩個請求全部完成的時候,等待信號也全部結(jié)束,這時表示任務(wù)全部完成。
不過如果更換一下需求會怎么樣呢,如果我們需要讓多個請求同步執(zhí)行要怎么做,首先我們需要開啟異步去管理,同樣請求也是異步方法,所以我們用這種方式不能讓請求同步執(zhí)行,這里需要使用線程依賴的方式操作,GCD線程依賴這里面也使用semaphore的方式去做,修改代碼如下

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    dispatch_semaphore_t semaphore0 = dispatch_semaphore_create(0);
    dispatch_semaphore_t semaphore1 = dispatch_semaphore_create(0);
    
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [NSThread sleepForTimeInterval:3];
            NSLog(@"完成1");
            dispatch_semaphore_signal(semaphore0);
        });
    });
    
    dispatch_group_async(group, queue, ^{
        dispatch_semaphore_wait(semaphore0,DISPATCH_TIME_FOREVER);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"完成2");
            dispatch_semaphore_signal(semaphore1);
        });
    });
    
    
    dispatch_group_notify(group, queue, ^{
        dispatch_semaphore_wait(semaphore1, DISPATCH_TIME_FOREVER);
        NSLog(@"全部完成");
    });

這里創(chuàng)建了兩個信號量,因為任務(wù)1模擬了時間更久,我們這里需要讓任務(wù)1先完成,那么我們在任務(wù)2中把任務(wù)1的信號量等待,直到任務(wù)1完成并增加信號量,再執(zhí)行任務(wù)2,結(jié)果如下

2018-09-05 10:27:02.725024+0800 GCDDemo[81615:22654482] 完成1
2018-09-05 10:27:04.727478+0800 GCDDemo[81615:22654482] 完成2
2018-09-05 10:27:04.727852+0800 GCDDemo[81615:22654485] 全部完成

可以看出我們的目的已經(jīng)實現(xiàn)了,但是如果有多個請求任務(wù)呢,必然需要創(chuàng)建多個信號量,按照需要的順序進行依賴,但是這種方法其實寫起來容易亂,最好的方式是使用NSOperationQueue添加線程依賴,但是這里不多加贅述,同樣后續(xù)文章中會詳細分析一次此案例,并使用其他方式解決這個問題。

dispatch_once
這個函數(shù)應(yīng)該也不會陌生,當(dāng)我們希望只會執(zhí)行一次的函數(shù)我們會使用dispatch_once,所有我們創(chuàng)建單例的時候也會使用到這個函數(shù)。

+ (instancetype)shareInstance {
    static Manager * manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [Manager new];
    });
    return manager;
}

這種單例不需要擔(dān)心線程問題,即使是多線程環(huán)境下也一定是安全的,onceToken會保證運行過程中這部分只會執(zhí)行一次。

GCD的實現(xiàn)

GCD的使用是十分方便的,這里探討一下它是如何實現(xiàn)的
C語言實現(xiàn)的用于管理追加Block的FIFO隊列
用于排他控制的atomic類型的輕量級信號
C語言實現(xiàn)的管理線程的容器
首先確定一下用于實現(xiàn)dispatch queue的軟件組件

組件和技術(shù)

我們所使用的全部api都處于libdispatch庫中,Dispatch Queue通過結(jié)構(gòu)體和鏈表實現(xiàn)FIFO隊列。FIFO通過dispatch_async等函數(shù)管理添加的block。
但是block并不是直接加入到FIFO隊列中,而是先加入Dispatch Continuation這個dispatch_continuation_t類型的結(jié)構(gòu)體中,再加入到隊列中,這個結(jié)構(gòu)體包含了block所屬的group等信息,也就是執(zhí)行上下文。

XNU內(nèi)核有4中workqueue,優(yōu)先級和Global Dispatch Queue的優(yōu)先級相同,當(dāng)在Global Dispatch Queue中執(zhí)行block時,libdispatch從FIFO隊列中取出Dispatch Continuation,調(diào)用pthread_workqueue_additem_np函數(shù),將自身信息,符合其優(yōu)先級的workqueue信息以及執(zhí)行回調(diào)函數(shù)等傳遞給參數(shù)。

pthread_work_queue_additem_np函數(shù)使用workq_kernreturn系統(tǒng)調(diào)用。通知workqueue應(yīng)當(dāng)執(zhí)行的項目。根據(jù)通知,XNU內(nèi)核基于系統(tǒng)判斷是否生成線程,如果是overcommit屬性則始終生成線程。

workqueue的線程執(zhí)行pthread_workqueue函數(shù),該函數(shù)調(diào)用libdispatch的回調(diào)函數(shù),在回調(diào)函數(shù)中執(zhí)行加入到Dispatch Continuation的Block。

Block執(zhí)行結(jié)束后,進行通知Dispatch Group結(jié)束、釋放Dispatch Continuation等處理。開始準(zhǔn)備執(zhí)行Global Dispatch Queue的下一個Block

以上就是Dispatch Queue的大致執(zhí)行過程。

Dispatch Source
GCD中除了常用的Dispatch Queue外,還有Dispatch Source,它具有很多種類型處理能力,最常見的就是使用定時器。

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    //創(chuàng)建一個dispatch_source_t類型變量,類型指定為定時器
    dispatch_source_set_timer(timer,DISPATCH_TIME_NOW,1.0*NSEC_PER_SEC, 0);
    //指定定時器執(zhí)行時間為每秒執(zhí)行一次
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"定時內(nèi)容");
        //每秒執(zhí)行的內(nèi)容
    });
    dispatch_source_set_cancel_handler(timer, ^{
        NSLog(@"定時取消");
        //取消定時器的回調(diào)
    });
    dispatch_resume(timer);
    //啟動定時器
    //dispatch_source_cancel(timer);
    //取消定時器

這里本身會存在一個問題,就是set_event_handle這個回調(diào)會不執(zhí)行,原因是當(dāng)執(zhí)行過作用域之后,這個source可能會被釋放掉,所以可以使用添加到屬性的方式放大source的作用域,保證定時器可以始終執(zhí)行

@property (nonatomic, strong) dispatch_source_t timer;

這里如果內(nèi)存管理語義使用了assign創(chuàng)建定時器,則會報出會被釋放的錯誤。

使用Dispatch Queue本身是不具備“取消”功能的,要么放棄取消,要么使用NSOperationQueue等方法,而Dispatch Source具備該功能,而且取消后執(zhí)行的處理可以使用block形式,這里也能看出Dispatch Source的強大功能。

到此為止關(guān)于GCD的這篇文章就結(jié)束了,如果文章中出現(xiàn)問題歡迎指出,并且如果有更優(yōu)秀的看法也歡迎提出或討論。

本文部分內(nèi)容參考自《Objective-C高級編程》一書,有興趣的小伙伴可以翻看一下

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容