【原創(chuàng)】iOS 多線程之GCD 及GCD API的使用

原創(chuàng),轉載請注明出處。

拋磚引玉。最近在復習了《Obj-C高級編程》這本書后,一方面記錄一下知識點,另一方便加了一些自己的理解。結合一些經(jīng)典的例子以及實際使用場景加深理解,權當學習交流之用。

需要了解的基本概念

1.同步執(zhí)行:阻塞當前線程。
2.異步執(zhí)行:不阻塞當前線程。
3.串行隊列:按照FIFO原則出列,一個一個的執(zhí)行。
4.并行隊列: 一起執(zhí)行。
后續(xù)內(nèi)容會再做解釋。

基礎API

dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

這是gcd 中最常見的兩個API,其中:

參數(shù)部分
  1. 第一個參數(shù) dispatch_queue_t代表放在哪個隊列執(zhí)行,系統(tǒng)提供了幾種隊列:
  • dispatch_get_global_queue(long identifier, unsigned long flags)全局并行隊列。第一個參數(shù)表示優(yōu)先級,最后一個參數(shù)寫0就可以了。系統(tǒng)提供了四種優(yōu)先級:(優(yōu)先級由高到低)
 *  - 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

一般dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)這么寫就行了。

  • dispatch_get_main_queue()主隊列,也就是主線程的執(zhí)行隊列。此為串行隊列。按照FIFO原則執(zhí)行。
  • 同樣我們也可以自定義隊列:dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
    第一個參數(shù)為隊列的名稱,《Obj-C高級編程》作者推薦使用應用ID這種逆序全程域名的命名方式:"com.example.gcd.MyConcurrentDispatchQueue",該名稱會出現(xiàn)在程序崩潰的crashlog中。便于排查問題。
    第二個參數(shù)代表隊列類型,NULLDISPATCH_QUEUE_SERIAL創(chuàng)建串行隊列。DISPATCH_QUEUE_CONCURRENT創(chuàng)建并行隊列。
  1. 第二個參數(shù)dispatch_block_t 是一個block,我們把需要使用GCD執(zhí)行的任務放在這個block里。
函數(shù)部分

dispatch_sync:代表同步執(zhí)行,對應基礎概念里的同步執(zhí)行。
這個方法會阻塞當前線程,將第二個參數(shù)block里的任務追加到第一個參數(shù)指定的隊列queue里執(zhí)行。直到block里的任務執(zhí)行完畢,程序才繼續(xù)往下運行。
假如當前線程的執(zhí)行隊列和第一個參數(shù)里的queue是同一個隊列,且都是串行隊列,那么就會造成死鎖。(見代碼1.1.1,1.1.2)

dispatch_async:代表異步執(zhí)行。不阻塞當前線程,即使用多個線程同時執(zhí)行多個處理。其中異步執(zhí)行一個并行隊列,XNU內(nèi)核會基于Dispatch Queue中的處理數(shù)、CPU核數(shù)以及CPU負荷等當前系統(tǒng)的狀態(tài)來決定要不要開辟新的線程,以及開辟多少個線程來處理。(代碼1.2)

代碼 1.1.1 死鎖

當前線程為主線程,當前隊列為主隊列。queue內(nèi)參數(shù)也為主隊列,同時他們都是串行隊列。那么按照剛剛我們總結的簡單理論,發(fā)生死鎖。

     NSLog(@"程序開始運行");
    //主線程阻塞,開始執(zhí)行block里的任務
    dispatch_sync(dispatch_get_main_queue(), ^{
        //task2
        NSLog(@"此句不執(zhí)行,加到主隊列中執(zhí)行,排在task3后面,按照FIFO原則需要等待 task3執(zhí)行完畢才能執(zhí)行");
    });
    // task2 等待task3, task3 等待 task2 .死鎖
    NSLog(@"此句不執(zhí)行,主線程主隊列死鎖");

我們將dispatch_get_main_queue 替換為dispatch_get_global_queue使得二者不為同一個串行隊列,則不會發(fā)生死鎖。同學們可以自行試驗。

代碼 1.1.2 死鎖 。

當前執(zhí)行隊列和dispatch_syncqueue參數(shù)都為同一個同步隊列。發(fā)生死鎖。

    NSLog(@"task1");
    dispatch_queue_t otherQueue = dispatch_queue_create("com.test.gcd.serialQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(otherQueue, ^{
        NSLog(@"task2");
        //發(fā)生死鎖,當前執(zhí)行隊列和dispatch_sync 執(zhí)行隊列相同且都是同步隊列
        dispatch_sync(otherQueue, ^{
            NSLog(@"task4");
            //task4 排在otherQueue執(zhí)行任務task5之后,需要task5執(zhí)行完畢才可以執(zhí)行。
        });
        //同步執(zhí)行,需要task4 執(zhí)行完畢才可以執(zhí)行task5。
        //task4 等待task5, task5 等待task4,發(fā)生死鎖。
        NSLog(@"task5");
    });
    //打印 task1 task2 task3(task2,task3順序不定)
    NSLog(@"task3");

同樣我們可以將dispatch_sync 里的otherQueue替換為任意一個非相同隊列,則不會發(fā)生死鎖。這里也不再贅述。

代碼 1.2 是否開啟新線程,以及開辟多少個。
    dispatch_queue_t queue1 = dispatch_queue_create("com.test.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("com.test.gcd.concurrentQueue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue1, ^{
        NSLog(@"thread%@",[NSThread currentThread]);
        dispatch_async(queue2, ^{
            NSLog(@"thread%@",[NSThread currentThread]);
        });
    });
    //有時打印
    //thread<NSThread: 0x6000037a1f40>{number = 4, name = (null)}
    //thread<NSThread: 0x6000037a1f40>{number = 4, name = (null)}
    //有時打印
    //thread<NSThread: 0x6000037a1f40>{number = 4, name = (null)}
    //thread<NSThread: 0x600003751e40>{number = 6, name = (null)}

以上試驗也可以驗證了這一理論,有時只需要開辟一個線程即可處理。有時需要開辟兩個新線程。

iOS和OS X的核心--XNU內(nèi)核決定應當使用的線程數(shù),并只生成所需的線程執(zhí)行處理。另外,當處理結束,應當執(zhí)行的處理數(shù)減少時,XNU內(nèi)核會結束不再需要的線程。XNU內(nèi)核僅使用Concurrent Dispatch Queue便可完美地管理并行執(zhí)行多個處理的線程。

dispatch_set_target_queue

dispatch_set_target_queue(dispatch_object_t object,dispatch_queue_t _Nullable queue);
此API,可以改變執(zhí)行隊列優(yōu)先級以及隊列類型。

  • Concurrent Dispatch Queue 改 Serial Dispatch Queue :
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t serialQueue = dispatch_queue_create("com.test.gcd.serialQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_set_target_queue(concurrentQueue, serialQueue);
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task1 thread:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task2 thread:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task3 thread:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task4 thread:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task5 thread:%@",[NSThread currentThread]);
    });

注釋掉 dispatch_set_target_queue這行,會無序打印task1-5。
加上后實際上執(zhí)行隊列由并行變成了串行執(zhí)行,task1-5按順序打印。

  • 改變隊列優(yōu)先級
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    
    dispatch_set_target_queue(concurrentQueue, globalQueue);

dispatch_queue_create 函數(shù)生成的Dispatch Queue 不管是Serial Queue 還是 Concurrent Queue,都使用與默認優(yōu)先級Global Dispatch Queue相同執(zhí)行優(yōu)先級的線程。而變更生成的Dispatch Queue 的執(zhí)行優(yōu)先級要使用dispatch_set_target_queue函數(shù)。

dispatch_after

dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
參數(shù):
1.時間(指定時間追加處理到Dispatch Queue)。
2.隊列。
3.任務。

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull *NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"3秒后追加到主線程隊列里執(zhí)行");
    });

因為主線程主隊列,在主線程RunLoop中執(zhí)行,所以在每隔1/60秒執(zhí)行的RunLoop中,任務最快在3秒后執(zhí)行,最慢在3+1/60秒后執(zhí)行。如果主隊列有大量處理,那么這個時間會更長。

Dispatch Group

在多個并行執(zhí)行的任務全部執(zhí)行完畢后,想要追加一個結束處理。這種場景往往比較常見。雖然可以通過別的方式實現(xiàn),但邏輯會變的復雜,代碼也不雅觀。這時候Dispatch Group就發(fā)揮作用了。

    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, ^{
        NSLog(@"task1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task3");
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"task Done");
    });
    //打印 (task1-3 無序)
    //task1
    //task2
    //task3
    //task Done

一個簡單的demo,在任務1-3完成后,執(zhí)行task Done

除了使用dispatch_group_notifyAPI 處理group任務結束外,還可以使用dispatch_group_wait函數(shù)。同樣的例子:

    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, ^{
        NSLog(@"task1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task3");
    });
    
    //也可以使用dispatch_group_wait 函數(shù)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"task Done");

DISPATCH_TIME_FOREVER代表永久等待。

我們也可以指定等待的時間,下例等待1秒,超過1秒不再等待。

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, ^{
        NSLog(@"task1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task3");
        for (int i = 0; i < 10000000; i++){
            @autoreleasepool{
                NSString* string = @"ab c";
                //生成autorelease對象
                NSArray* array = [string componentsSeparatedByString:string];
            }
        }
    });
    
    //DISPATCH_TIME_FOREVER 永久等待,同樣我們可以設置等待的時間
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull*NSEC_PER_SEC);
    long result = dispatch_group_wait(group, time);
    if (result == 0) {
        // 屬于Dispatch Group 的全部處理執(zhí)行結束
        NSLog(@"task Done");
    } else {
        // 屬于Dispatch Group 的某個處理還在執(zhí)行中
        NSLog(@"task Doing");
    }

通過dispatch_group_wait返回值可以判斷,是group任務在設置的超時時間內(nèi)完成,還是超時未完成。 result ==0 代表全部處理完成,非0代表執(zhí)行超時了。

但假如Dispatch Queue 里的任務是一個個網(wǎng)絡請求的話,由于網(wǎng)絡請求是異步執(zhí)行,那么實際達不到我們想要的在所有請求完畢后執(zhí)行某段代碼的目的。那么這時就可以借助信號量Dispatch Semaphore來完成了。

Dispatch Semaphore

  • dispatch_semaphore_create(long value) 創(chuàng)建一個信號量。
  • dispatch_semaphore_signal(dispatch_semaphore_t dsema) 信號量加1
  • dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) 等待信號大0時執(zhí)行,并對信號量進行減一操作。
1.上述在Dispatch Queue里并行執(zhí)行多個網(wǎng)絡請求的情況,想要在所有請求都完成的情況執(zhí)行某段代碼就可以使用Dispatch Semaphore了。
- (void)requestDemo{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    __weak typeof(self) weak_self = self;
    dispatch_group_async(group, queue, ^{
        NSLog(@"請求任務A");
        [weak_self requestA];
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"請求任務B");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"請求任務C");
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"所有請求完成");
    });
}

- (void)requestA{
    //    用于GCD Group 以及 NSOperationQueue中設置依賴關系的任務,因為網(wǎng)絡請求異步執(zhí)行,
    //    不會阻塞當前線程,達不到按序執(zhí)行的效果。
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    //    [異步請求:{
    //        成功: dispatch_semaphore_signal(sema);
    //        失敗: dispatch_semaphore_signal(sema);
    //    }];
    //一直等待到信號量大于0才執(zhí)行,并減1
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}

異步請求方法前創(chuàng)建為0的信號量,請求結束后信號量+1,dispatch_semaphore_wait會等到信號量大于才繼續(xù)運行。整個請求模塊會在dispatch_semaphore_wait可以繼續(xù)運行才標記為block任務結束。

2.控制異步執(zhí)行Dispatch Concurrent Queue最大并發(fā)數(shù)。

眾所周知NSOperationQueue便于管理多線程,可以設置maxConcurrentOperationCount來控制多線程執(zhí)行的最大并發(fā)數(shù)。那么GCD要如何控制最大并發(fā)呢?這時Dispatch Semaphore又發(fā)揮作用了。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //初始信號量1 ,這里1可以為n
    dispatch_semaphore_t sema = dispatch_semaphore_create(1);
    for (NSInteger i = 0; i < 10; i++) {
        //大于0執(zhí)行,并減1
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        dispatch_async(queue, ^{
            NSLog(@"%ld",i);
            //任務完成,信號量加1
            dispatch_semaphore_signal(sema);
        });
    }
    //按順序打印0-9

通過設置dispatch_semaphore_create (1)設置最大并發(fā)1,那么實際上就把并發(fā)隊列設置成了一個串行隊列。dispatch_semaphore_create (n)則最大并發(fā)為n,如果n設置的很大,實際上達不到n。因為蘋果內(nèi)核決定了此次GCD執(zhí)行的并發(fā)隊列所需要的線程數(shù)。

未完待續(xù)。。。
后續(xù)補充:

dispatch_barrier_async
dispatch_apply
dispatch_once

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

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

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