iOS多執行緒:GCD詳解

夜幕降臨耶發表於2020-03-15

多執行緒開發是日常開發任務中不可缺少的一部分,在iOS開發中常用到的多執行緒開發技術有GCD、NSOperation、NSThread,本文主要講解多線系列文章中關於NSOperation的相關知識和使用詳解。

  1. iOS多執行緒:GCD詳解
  2. iOS多執行緒:NSOperation詳解

1、GCD簡介

GCD對於iOS開發者來說並不陌生,在實際開發中我們會經常用到GCD進行多執行緒的處理,那麼GCD是什麼呢?

Grand Central Dispatch(GCD) 是 Apple 開發的一個多核程式設計的較新的解決方法。它主要用於優化應用程式以支援多核處理器以及其他對稱多處理系統。它是一個線上程池模式的基礎上執行的併發任務。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

GCD有著很明顯的優勢,正是這些優勢才使得GCD在處理多執行緒問題有著舉足輕重的地位。

  1. GCD是apple為多核的並行運算提出的解決方案。
  2. GCD能較好的利用CPU核心資源。
  3. GCD不需要開發者去管理執行緒的生命週期。
  4. 使用簡便,開發者只需要告訴GCD執行什麼任務,並不需要編寫任何執行緒管理程式碼。

2、GCD任務和佇列

相信很多初級開發者會對GCD任務和佇列之間的關係理解含糊不清,實際上佇列只是提供了儲存任務的容器。為了更好的理解GCD,很有必要先了解任務佇列的概念。

2.1 GCD任務

任務就是需要執行的操作,是GCD中放在block中線上程中執行的那段程式碼。任務的執行的方式有同步執行非同步執行兩中執行方式。兩者的主要區別是是否等待佇列的任務執行結束,以及是否具備開啟新執行緒的能力

  • 同步執行(sync):同步新增任務到佇列中,在佇列之前的任務執行結束之前會一直等待;同步執行的任務只能在當前執行緒中執行,不具備開啟新執行緒的能力。
  • 非同步執行(async):非同步新增任務到佇列中,並需要理會佇列中其他的任務,新增即執行;非同步執行可以在新的執行緒中執行,具備開啟新的執行緒的能力。

2.2 GCD的佇列

佇列:佇列是一種特殊的線性表,佇列中允許插入操作的一端稱為隊尾,允許刪除操作的一端稱為隊頭,是一種先進先出的結構。在GCD裡面佇列是指執行任務的等待佇列,是用來存放任務的。按照佇列的結構特性,新任務總是插入在佇列的末尾,而任務的執行總是從佇列的對頭輸出,每讀取一個任務,則從佇列中釋放一個任務。GCD的佇列分為序列佇列併發佇列兩種,兩者都符合 FIFO(先進先出)的原則。兩者的主要區別是:執行順序不同,以及開啟執行緒數不同。

  • 序列佇列:只開啟一個執行緒,每次只能有一個任務執行,等待執行完畢後才會執行下一個任務。
  • 併發佇列:可以讓對個任務同時執行,也就是開啟多個執行緒,讓多個任務同時執行。

兩者之間區別如下圖所示:

序列佇列

併發佇列

3、GCD基本使用

GCD的使用很簡單,首先建立一個佇列,然後向佇列中追加任務,系統會根據任務的型別執行任務。

3.1、佇列的建立

  1. 佇列的建立很簡單,只需要呼叫dispatch_queue_create方法傳入相對應的引數便可。這個方法有兩個引數:
  • 第一個參數列示佇列的唯一標識,可以傳空。
  • 第二個引數用來識別是序列佇列還是併發佇列。DISPATCH_QUEUE_SERIAL 表示序列佇列,DISPATCH_QUEUE_CONCURRENT表示併發佇列。
// 建立序列佇列
dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_SERIAL);
// 建立併發佇列
dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_CONCURRENT);
複製程式碼
  1. GCD預設提供一種全域性併發佇列,呼叫 dispatch_get_global_queue方法來獲取全域性併發佇列。這個方法需要傳入兩個引數。
  • 第一個引數是一個長整型型別的引數,表示佇列優先順序,有DISPATCH_QUEUE_PRIORITY_HIGHDISPATCH_QUEUE_PRIORITY_LOWDISPATCH_QUEUE_PRIORITY_BACKGROUNDDISPATCH_QUEUE_PRIORITY_DEFAULT四個選項,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT
  • 第二個引數暫時沒用,用 0 即可。
// 獲取全域性併發佇列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
複製程式碼
  1. GCD預設提供了主佇列,呼叫dispatch_get_main_queue方法獲取,所有放在主佇列中的任務都會在主執行緒中執行。主佇列是一種序列佇列。
// 主隊
dispatch_queue_t mainQueue = dispatch_get_main_queue();
複製程式碼

3.2、建立任務

GCD呼叫dispatch_sync建立同步任務,呼叫dispatch_async建立非同步任務。任務的內容都是在block程式碼塊中。

//非同步任務
dispatch_async(queue, ^{
   //非同步執行的程式碼
});
 
//同步任務   
dispatch_sync(queue, ^{
   //同步執行的程式碼
});
複製程式碼

3.3、任務和佇列的組合

建立的任務需要放在佇列中去執行,同時考慮到主佇列的特殊性,那麼在不考慮巢狀任務的情況下就會存在同步任務+序列佇列、同步任務+併發佇列、非同步任務+序列佇列、非同步任務+併發佇列、主佇列+同步任務、主佇列+非同步任務六種組合,下面我們來分析下這幾種組合。

  • 同步任務+序列佇列:同步任務不會開啟新的執行緒,任務序列執行。
  • 同步任務+併發佇列:同步任務不會開啟新的執行緒,雖然任務在併發佇列中,但是系統只預設開啟了一個主執行緒,沒有開啟子執行緒,所以任務序列執行。
  • 非同步任務+序列佇列:非同步任務有開啟新的執行緒,任務序列執行。
  • 非同步任務+併發佇列:非同步任務有開啟新的執行緒,任務併發執行。
  • 主佇列+同步任務:主佇列是一種序列佇列,任務在主執行緒中序列執行,將同步任務新增到主佇列中會造成追加的同步任務和主執行緒中的任務相互等待阻塞主執行緒,導致死鎖。
  • 主佇列+非同步任務:主佇列是一種序列佇列,任務在主執行緒中序列執行,即使是追加的非同步任務也不會開啟新的執行緒,任務序列執行。

下面我們來看看各種組合之間的使用。

3.4、GCD的基礎使用

3.4.1、同步任務+序列佇列
- (void)syncTaskWithSerial {
    NSLog(@"currentThread:%@", [NSThread currentThread]);
    dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        NSLog(@"currentThread-1:%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"currentThread-2:%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"currentThread-3:%@", [NSThread currentThread]);
    });
}
複製程式碼

列印結果: 2020-03-12 21:34:25.807965+0800 ThreadDemo[51144:6948582] currentThread:<NSThread: 0x600001739100>{number = 1, name = main} 2020-03-12 21:34:25.808231+0800 ThreadDemo[51144:6948582] currentThread-1:<NSThread: 0x600001739100>{number = 1, name = main} 2020-03-12 21:34:25.808467+0800 ThreadDemo[51144:6948582] currentThread-2:<NSThread: 0x600001739100>{number = 1, name = main} 2020-03-12 21:34:25.808669+0800 ThreadDemo[51144:6948582] currentThread-3:<NSThread: 0x600001739100>{number = 1, name = main}

從上面程式碼執行的結果可以看出,並沒有開啟新的執行緒,任務是按順序執行的。

3.4.2、同步任務+併發佇列
- (void)syncTaskWithConcurrent {
    NSLog(@"current thread:%@", [NSThread currentThread]);
    dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(queue, ^{
        NSLog(@"current thread-1:%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"current thread-2:%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"current thread-3:%@", [NSThread currentThread]);
    });
}
複製程式碼

列印結果: 2020-03-12 21:39:45.931001+0800 ThreadDemo[51225:6953218] current thread:<NSThread: 0x600002abe0c0>{number = 1, name = main} 2020-03-12 21:39:45.931259+0800 ThreadDemo[51225:6953218] current thread-1:<NSThread: 0x600002abe0c0>{number = 1, name = main} 2020-03-12 21:39:45.931442+0800 ThreadDemo[51225:6953218] current thread-2:<NSThread: 0x600002abe0c0>{number = 1, name = main} 2020-03-12 21:39:45.931606+0800 ThreadDemo[51225:6953218] current thread-3:<NSThread: 0x600002abe0c0>{number = 1, name = main}

從上面程式碼執行的結果可以看出,同步任務不會開啟新的執行緒,雖然任務在併發佇列中,但是系統只預設開啟了一個主執行緒,沒有開啟子執行緒,所以任務序列執行。

3.4.3、非同步任務+序列佇列
- (void)asyncTaskWithSeria{
    NSLog(@"current thread:%@", [NSThread currentThread]);
    dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"current thread-1:%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"current thread-2:%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"current thread-3:%@", [NSThread currentThread]);
    });
    NSLog(@"4");
}
複製程式碼

列印結果: 2020-03-12 21:44:22.369058+0800 ThreadDemo[51283:6957598] current thread:<NSThread: 0x6000031891c0>{number = 1, name = main} 2020-03-12 21:44:22.369279+0800 ThreadDemo[51283:6957598] 4 2020-03-12 21:44:22.369346+0800 ThreadDemo[51283:6958684] current thread-1:<NSThread: 0x6000031acb80>{number = 7, name = (null)} 2020-03-12 21:44:22.369511+0800 ThreadDemo[51283:6958684] current thread-2:<NSThread: 0x6000031acb80>{number = 7, name = (null)} 2020-03-12 21:44:22.369675+0800 ThreadDemo[51283:6958684] current thread-3:<NSThread: 0x6000031acb80>{number = 7, name = (null)}

從上面程式碼執行的結果可以看出,開啟了一個新的執行緒,說明非同步任務具備開啟新的執行緒的能力,但是由於任務是在序列佇列中執行的,所以任務是順序執行的。

3.4.4、非同步任務+併發佇列
- (void)asyncTaskWithConcurrent{
    NSLog(@"current thread:%@", [NSThread currentThread]);
    dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"current thread-1:%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"current thread-2:%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"current thread-3:%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"current thread-4:%@", [NSThread currentThread]);
    });
}
複製程式碼

列印結果: 2020-03-12 21:59:36.511770+0800 ThreadDemo[51635:6976397] current thread:<NSThread: 0x60000024ed00>{number = 1, name = main} 2020-03-12 21:59:36.512015+0800 ThreadDemo[51635:6976575] current thread-2:<NSThread: 0x600000214ec0>{number = 5, name = (null)} 2020-03-12 21:59:36.512011+0800 ThreadDemo[51635:6976577] current thread-1:<NSThread: 0x600000215700>{number = 4, name = (null)} 2020-03-12 21:59:36.512028+0800 ThreadDemo[51635:6976580] current thread-3:<NSThread: 0x60000021f2c0>{number = 6, name = (null)} 2020-03-12 21:59:36.512035+0800 ThreadDemo[51635:6976578] current thread-4:<NSThread: 0x60000023b340>{number = 7, name = (null)}

從上面程式碼的執行結果可以看出,生成了多個執行緒,並且任務是隨機執行(併發執行)的。

3.4.5、主佇列+同步任務
-(void)syncTaskWithMain{
    NSLog(@"currentThread---%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);
    });
    
    dispatch_sync(queue, ^{
        NSLog(@"2---%@",[NSThread currentThread]);
    });
    
    dispatch_sync(queue, ^{
        NSLog(@"3---%@",[NSThread currentThread]);
    });
    
    NSLog(@"4");
}
複製程式碼

列印結果: 2020-03-12 22:05:01.689594+0800 ThreadDemo[51754:6982402] currentThread---<NSThread: 0x600003eaed00>{number = 1, name = main} (lldb)

很明顯上面這段程式碼執行崩潰了,這是因為我們在主執行緒中執行 syncTaskWithMain 方法,相當於把 syncTaskWithMain 任務放到了主執行緒的佇列中。而 同步執行會等待當前佇列中的任務執行完畢,才會接著執行。那麼當我們把 任務 1 追加到主佇列中,任務 1 就在等待主執行緒處理完 syncTaskWithMain 任務。而syncMain 任務需要等待任務 1 執行完畢,這樣就形成了相互等待的情況,產生了死鎖。

3.4.6、主佇列+非同步任務
-(void)asyncTaskWithMain{
    NSLog(@"currentThread---%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"2---%@",[NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"3---%@",[NSThread currentThread]);
    });
    
    NSLog(@"4");
}
複製程式碼

列印結果: 2020-03-12 22:09:49.285203+0800 ThreadDemo[51832:6986908] currentThread---<NSThread: 0x600000aff3c0>{number = 1, name = main} 2020-03-12 22:09:49.285539+0800 ThreadDemo[51832:6986908] 4 2020-03-12 22:09:49.326310+0800 ThreadDemo[51832:6986908] 1---<NSThread: 0x600000aff3c0>{number = 1, name = main} 2020-03-12 22:09:49.326749+0800 ThreadDemo[51832:6986908] 2---<NSThread: 0x600000aff3c0>{number = 1, name = main} 2020-03-12 22:09:49.326988+0800 ThreadDemo[51832:6986908] 3---<NSThread: 0x600000aff3c0>{number = 1, name = main}

從上面程式碼的執行結果可以看出,雖然是非同步任務,但是並沒有開啟新的執行緒,任然是在主執行緒中執行,並且任務是順序執行的。

3.5、任務巢狀使用

關於任務的巢狀使用有多種情況,這裡做一個簡單的總結,

區別 『非同步執行+併發佇列』巢狀『同一個併發佇列』 『同步執行+併發佇列』巢狀『同一個併發佇列』 『非同步執行+序列佇列』巢狀『同一個序列佇列』 『同步執行+序列佇列』巢狀『同一個序列佇列』
同步 沒有開啟新的執行緒,序列執行任務 沒有開啟新執行緒,序列執行任務 死鎖卡住不執行 死鎖卡住不執行
非同步 有開啟新執行緒,併發執行任務 有開啟新執行緒,併發執行任務 有開啟新執行緒(1 條),序列執行任務 有開啟新執行緒(1 條),序列執行任務

對於**『非同步執行+序列佇列』巢狀『同一個序列佇列』**造成死鎖的情況請看如下程式碼:

dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_SERIAL);
//非同步任務A
dispatch_async(queue, ^{   
    //同步任務B
    dispatch_sync(queue, ^{  
        NSLog(@"任務C---%@",[NSThread currentThread]);
    });
});
複製程式碼

首先非同步任務A進入到佇列中,同步任務B對於非同步任務A來說是程式碼執行部分,C對於同步任務B來說是程式碼執行部分,因為是在序列佇列中,任務是序列執行的,根據佇列先進先出原則,首先需要把任務A取出執行,即執行B的部分,但是B依賴C的執行,而C等待著B執行完成後執行,這樣就形成了一個相互等待,造成死鎖卡死。

**同步執行+序列佇列』巢狀『同一個序列佇列』**造成死鎖的情況同理分析。

4、GCD執行緒間的通訊

在 iOS 開發過程中,我們在主執行緒進行UI重新整理,把圖片下載、檔案上傳、網路請求等一些耗時的操作放在其他的執行緒,當這些耗時的操作完成後需要將資料同步給UI,就需要回到主執行緒重新整理UI,那麼就要用到執行緒之間的通訊。GCD提供了非常簡便的方法進行執行緒間的通訊。

- (void)communication {
    dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);
        // 模擬耗時操作
        sleep(2);
        // 回到主執行緒
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"2---%@",[NSThread currentThread]);      // 列印當前執行緒
        });
    });
}
複製程式碼

列印結果 2020-03-13 00:06:07.632014+0800 ThreadDemo[53480:7074047] 1---<NSThread: 0x60000063e480>{number = 5, name = (null)} 2020-03-13 00:06:09.633041+0800 ThreadDemo[53480:7073841] 2---<NSThread: 0x60000061a180>{number = 1, name = main}

從上面程式碼執行的結果可以看出,1是在子執行緒中執行的,隔2秒後列印2,2是在主執行緒中執行的。

5、GCD中的函式方法

GCD中提供了諸多的函式方法供開發者呼叫,我們一起來看下這些方法的使用。

5.1、柵欄方法

有的時候我們需要非同步執行兩組操作,等待第一組執行完成後才回去執行第二組操作,這個時候柵欄方法就起作用了。 柵欄方法(dispatch_barrier_asyncdispatch_barrier_sync)會等前邊追加到佇列中的任務執行完畢後,再將制定的任務追加到佇列中,然後等到dispatch_barrier_asyncdispatch_barrier_sync方法追加的任務執行完畢後才會去執行後邊追加到佇列中的任務,簡單來說dispatch_barrier_asyncdispatch_barrier_sync將非同步任務分成了兩個組,執行完第一組後,再執行自己,然後執行佇列中剩餘的任務。唯一不同的是dispatch_barrier_async不會阻塞執行緒。

iOS多執行緒:GCD詳解

看如下程式碼:

- (void)barrierTask {
    dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"start");
    dispatch_async(queue, ^{
        NSLog(@"currentThread-1:%@", [NSThread currentThread]);
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"currentThread-2:%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];
    });
    NSLog(@"pause");
    dispatch_async(queue, ^{
        NSLog(@"currentThread-3:%@", [NSThread currentThread]);
    });
    NSLog(@"end");
}
複製程式碼

列印結果 2020-03-13 00:31:53.247514+0800 ThreadDemo[54101:7100220] start 2020-03-13 00:31:53.247730+0800 ThreadDemo[54101:7100220] pause 2020-03-13 00:31:53.247809+0800 ThreadDemo[54101:7100396] currentThread-1:<NSThread: 0x600003b8db00>{number = 5, name = (null)} 2020-03-13 00:31:53.247883+0800 ThreadDemo[54101:7100220] end 2020-03-13 00:31:53.247991+0800 ThreadDemo[54101:7100396] currentThread-2:<NSThread: 0x600003b8db00>{number = 5, name = (null)} 2020-03-13 00:31:55.250622+0800 ThreadDemo[54101:7100396] currentThread-3:<NSThread: 0x600003b8db00>{number = 5, name = (null)}

從上面的程式碼執行結果可以看出,start、pause、end都是在2執行答應的,說明dispatch_barrier_async並沒有阻塞執行緒,3是在2列印兩秒後列印的。

如果把dispatch_barrier_async換成dispatch_barrier_sync列印結果會是怎麼樣呢?

- (void)barrierTask {
    dispatch_queue_t queue = dispatch_queue_create("com.shen.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"start");
    dispatch_async(queue, ^{
        NSLog(@"currentThread-1:%@", [NSThread currentThread]);
    });
    dispatch_barrier_sync(queue, ^{
        NSLog(@"currentThread-2:%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];
    });
    NSLog(@"pause");
    dispatch_async(queue, ^{
        NSLog(@"currentThread-3:%@", [NSThread currentThread]);
    });
    NSLog(@"end");
}
複製程式碼

列印結果 2020-03-13 00:35:01.460109+0800 ThreadDemo[54171:7103212] start 2020-03-13 00:35:01.460408+0800 ThreadDemo[54171:7103379] currentThread-1:<NSThread: 0x600002508540>{number = 6, name = (null)} 2020-03-13 00:35:01.460588+0800 ThreadDemo[54171:7103212] currentThread-2:<NSThread: 0x600002570880>{number = 1, name = main} 2020-03-13 00:35:03.461678+0800 ThreadDemo[54171:7103212] pause 2020-03-13 00:35:03.462012+0800 ThreadDemo[54171:7103212] end 2020-03-13 00:35:03.462145+0800 ThreadDemo[54171:7103379] currentThread-3:<NSThread: 0x600002508540>{number = 6, name = (null)}

從上面程式碼執行的結果可以看出,pause和end是在2之後列印的,說明dispatch_barrier_sync阻塞了執行緒,需要等待dispatch_barrier_sync執行完成後才會往後執行。

5.2、延時執行方法-dispatch_after

延時執行任務相信對於iOS開發者來說並不陌生,我們經常遇到需要在延後指定之間後執行某個操作的需求,那麼這種需求用GCD來實現是很方便的。GCD的延時執行的函式是dispatch_after。需要注意的是:dispatch_after 方法並不是在指定時間之後才開始執行處理,而是在指定時間之後將任務追加到主佇列中。嚴格來說,這個時間並不是絕對準確的,但想要大致延遲執行任務,dispatch_after 方法是很有效的。

-(void)afterTask{
    NSLog(@"begin");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"after---%@",[NSThread currentThread]);
    });
}
複製程式碼

列印結果 2020-03-13 00:43:47.787769+0800 ThreadDemo[54378:7111012] begin 2020-03-13 00:43:50.788086+0800 ThreadDemo[54378:7111012] after---<NSThread: 0x60000042ddc0>{number = 1, name = main}

從上面程式碼的執行結果可以看出afer是在begin列印後3秒才列印的。

5.3、一次性程式碼-dispatch_once

GCD提供了只執行一次的方法dispatch_once,這個方法在我們建立單例的時候回經常用到。dispatch_once方法可以保證一段程式碼在程式執行過程中只被呼叫一次,而且在多執行緒環境下可以保證執行緒安全。

+ (instancetype)shareInstance{
    static Test *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [Test alloc]init];
    });
    return instance;
}
複製程式碼

5.5、排程組

排程組簡單來說就是把非同步執行的任務進行分組,等待所有的分組任務都執行完畢後再回到指定的執行緒執行任務。呼叫組使用dispatch_group_create來建立一個分組,dispatch_group_async方法先把任務新增到佇列中,然後將佇列方到排程組中,或者也可以使用dispatch_group_enterdispatch_group_leave捉對實現將佇列新增到排程組。呼叫dispatch_group_notify方法回到指定執行緒執行任務,或者呼叫dispatch_group_wait阻塞當前執行緒。

- (void)groupNotifyTest{
    NSLog(@"current thread:%@", [NSThread currentThread]);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread-1:%@", [NSThread currentThread]);
    });

    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread-2:%@", [NSThread currentThread]);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread-3:%@", [NSThread currentThread]);
        NSLog(@"group-end");
    });

    //會阻塞執行緒,
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"dispatch_group_wait後繼續執行任務");
}
複製程式碼

列印結果: 2020-03-14 23:58:09.871023+0800 ThreadDemo[77370:8259501] current thread:<NSThread: 0x6000019fe480>{number = 1, name = main} 2020-03-14 23:58:11.874345+0800 ThreadDemo[77370:8260290] thread-2:<NSThread: 0x600001996e80>{number = 7, name = (null)} 2020-03-14 23:58:11.874343+0800 ThreadDemo[77370:8259684] thread-1:<NSThread: 0x6000019a55c0>{number = 5, name = (null)} 2020-03-14 23:58:11.874672+0800 ThreadDemo[77370:8259501] dispatch_group_wait後繼續執行任務 2020-03-14 23:58:13.877077+0800 ThreadDemo[77370:8259501] thread-3:<NSThread: 0x6000019fe480>{number = 1, name = main} 2020-03-14 23:58:13.877365+0800 ThreadDemo[77370:8259501] group-end

在這裡需要說明的一點是dispatch_group_wait,該方法需要傳入兩個引數,第一個引數是group即排程組,第二個引數是timerout即指定等待的時間。一旦呼叫dispatch_group_wait函式,該函式就處理呼叫的狀態而不返回值,只有當函式的currentThread停止,或到達wait函式指定的等待的時間,或Dispatch Group中的操作全部執行完畢之前,執行該函式的執行緒停止。當指定timeout為DISPATCH_TIME_FOREVER時就意味著永久等待;當指定timeout為DISPATCH_TIME_NOW時就意味不用任何等待即可判定屬於Dispatch Group的處理是否全部執行結束。如果dispatch_group_wait函式返回值不為0,就意味著雖然經過了指定的時間,但Dispatch Group中的操作並未全部執行完畢。如果dispatch_group_wait函式返回值為0,就意味著Dispatch Group中的操作全部執行完畢。

下面看dispatch_group_enterdispatch_group_leave捉對實現將佇列新增到排程組的情況。dispatch_group_enter 標誌著在group的任務數+1,dispatch_group_leave 標誌著在group中的任務數-1,表示已經完成了一個任務。

- (void)groupEnterTest {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_enter(group);

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread_1:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread_2:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread_3:%@", [NSThread currentThread]);
        NSLog(@"group_end");
    });
}
複製程式碼

列印結果: 2020-03-15 00:07:17.293333+0800 ThreadDemo[77525:8269621] thread_2:<NSThread: 0x600002434ac0>{number = 7, name = (null)} 2020-03-15 00:07:17.293320+0800 ThreadDemo[77525:8269427] thread_1:<NSThread: 0x60000241b9c0>{number = 3, name = (null)} 2020-03-15 00:07:19.294186+0800 ThreadDemo[77525:8269235] thread_3:<NSThread: 0x600002450b80>{number = 1, name = main} 2020-03-15 00:07:19.294485+0800 ThreadDemo[77525:8269235] group_end

需要注意的是dispatch_group_enterdispatch_group_leave捉對出現的。

5.6、訊號量

GCD中的訊號量是指的Dispatch Semaphore,是持有計數的訊號。當訊號量小於0時就會一直等待即阻塞所線上程,否則就可以正常執行。訊號量可以保持執行緒的同步,將非同步執行任務轉換成同步任務執行, 同時保持執行緒的安全

Dispatch Semaphore提供了三個方法:

  • dispatch_semaphore_create:建立一個 Semaphore 並初始化訊號的總量
  • dispatch_semaphore_signal:傳送一個訊號,讓訊號總量加 1
  • dispatch_semaphore_wait:可以使總訊號量減 1,訊號總量小於 0 時就會一直等待(阻塞所線上程),否則就可以正常執行。
- (void)semaphoreTest {
   dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block int a = 0;
    while (a < 5) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"裡面的a的值:%d-----%@", a, [NSThread currentThread]);
            dispatch_semaphore_signal(semaphore);
            a++;
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }
    NSLog(@"外面的a的值:%d", a);
}
複製程式碼

列印結果: 2020-03-15 00:44:53.005899+0800 ThreadDemo[78303:8318841] 裡面的a的值:0-----<NSThread: 0x600003c222c0>{number = 3, name = (null)} 2020-03-15 00:44:53.006161+0800 ThreadDemo[78303:8318841] 裡面的a的值:1-----<NSThread: 0x600003c222c0>{number = 3, name = (null)} 2020-03-15 00:44:53.006354+0800 ThreadDemo[78303:8318841] 裡面的a的值:2-----<NSThread: 0x600003c222c0>{number = 3, name = (null)} 2020-03-15 00:44:53.006551+0800 ThreadDemo[78303:8318841] 裡面的a的值:3-----<NSThread: 0x600003c222c0>{number = 3, name = (null)} 2020-03-15 00:44:53.006727+0800 ThreadDemo[78303:8318841] 裡面的a的值:4-----<NSThread: 0x600003c222c0>{number = 3, name = (null)} 2020-03-15 00:44:53.006862+0800 ThreadDemo[78303:8318672] 外面的a的值:5

5.7、排程源-Dispatch_source

排程源是協調特殊低階別系統事件處理的基本資料型別。GCD支援諸如定時器排程源、訊號排程源、描述符排程源、程式排程源、埠排程源、自定義排程源等。排程源有這一系列的成熟API,在這裡就不多做描述,詳細可以查閱官方的文件Dispatch Sources

相關文章