本文用來介紹 iOS 多線程中 GCD 的相關知識以及使用方法。這大概是史上最詳細、清晰的關于 GCD 的詳細講解+總結的文章了。通過本文,您將了解到:
1. GCD 簡介
2. GCD 任務和隊列
3. GCD 的使用步驟
4. GCD 的基本使用(6種不同組合區別)
5. GCD 線程間的通信
6. GCD 的其他方法(柵欄方法:dispatch_barrier_async、延時執行方法:dispatch_after、一次性代碼(只執行一次):dispatch_once、快速迭代方法:dispatch_apply、隊列組:dispatch_group、信號量:dispatch_semaphore)文中 Demo 我已放在了 Github 上,Demo 鏈接:傳送門
1. GCD 簡介
什么是 GCD 呢?我們先來看看百度百科的解釋簡單了解下概念
引自百度百科
Grand Central Dispatch(GCD) 是 Apple 開發的一個多核編程的較新的解決方法。它主要用于優化應用程序以支持多核處理器以及其他對稱多處理系統。它是一個在線程池模式的基礎上執行的并發任務。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。
為什么要用 GCD 呢?
因為 GCD 有很多好處啊,具體如下:
- GCD 可用于多核的并行運算
- GCD 會自動利用更多的 CPU 內核(比如雙核、四核)
- GCD 會自動管理線程的生命周期(創建線程、調度任務、銷毀線程)
- 程序員只需要告訴 GCD 想要執行什么任務,不需要編寫任何線程管理代碼
既然 GCD 有這么多的好處,那么下面我們就來系統的學習一下 GCD 的使用方法。
2. GCD 任務和隊列
學習 GCD 之前,先來了解 GCD 中兩個核心概念:任務和隊列。
任務:就是執行操作的意思,換句話說就是你在線程中執行的那段代碼。在 GCD 中是放在 block 中的。執行任務有兩種方式:同步執行(sync)和異步執行(async)。兩者的主要區別是:是否等待隊列的任務執行結束,以及是否具備開啟新線程的能力。
-
同步執行(sync):
- 同步添加任務到指定的隊列中,在添加的任務執行結束之前,會一直等待,直到隊列里面的任務完成之后再繼續執行。
- 只能在當前線程中執行任務,不具備開啟新線程的能力。
-
異步執行(async):
- 異步添加任務到指定的隊列中,它不會做任何等待,可以繼續執行任務。
- 可以在新的線程中執行任務,具備開啟新線程的能力。
舉個簡單例子:你要打電話給小明和小白。
同步執行就是,你打電話給小明的時候,不能同時打給小白,等到給小明打完了,才能打給小白(等待任務執行結束)。而且只能用當前的電話(不具備開啟新線程的能力)。
而異步執行就是,你打電話給小明的時候,不等和小明通話結束,還能直接給小白打電話,不用等著和小明通話結束再打(不用等待任務執行結束)。除了當前電話,你還可以使用其他所能使用的電話(具備開啟新線程的能力)。
注意:異步執行(async)雖然具有開啟新線程的能力,但是并不一定開啟新線程。這跟任務所指定的隊列類型有關(下面會講)。
隊列(Dispatch Queue):這里的隊列指執行任務的等待隊列,即用來存放任務的隊列。隊列是一種特殊的線性表,采用 FIFO(先進先出)的原則,即新任務總是被插入到隊列的末尾,而讀取任務的時候總是從隊列的頭部開始讀取。每讀取一個任務,則從隊列中釋放一個任務。隊列的結構可參考下圖:
在 GCD 中有兩種隊列:串行隊列和并發隊列。兩者都符合 FIFO(先進先出)的原則。兩者的主要區別是:執行順序不同,以及開啟線程數不同。
-
串行隊列(Serial Dispatch Queue):
- 每次只有一個任務被執行。讓任務一個接著一個地執行。(只開啟一個線程,一個任務執行完畢后,再執行下一個任務)
-
并發隊列(Concurrent Dispatch Queue):
- 可以讓多個任務并發(同時)執行。(可以開啟多個線程,并且同時執行任務)
注意:并發隊列的并發功能只有在異步(dispatch_async)函數下才有效
兩者具體區別如下兩圖所示。
3. GCD 的使用步驟
GCD 的使用步驟其實很簡單,只有兩步。
- 創建一個隊列(串行隊列或并發隊列)
- 將任務追加到任務的等待隊列中,然后系統就會根據任務類型執行任務(同步執行或異步執行)
下邊來看看隊列的創建方法/獲取方法,以及任務的創建方法。
3.1 隊列的創建方法/獲取方法
- 可以使用
dispatch_queue_create
來創建隊列,需要傳入兩個參數,第一個參數表示隊列的唯一標識符,用于 DEBUG,可為空,Dispatch Queue 的名稱推薦使用應用程序 ID 這種逆序全程域名;第二個參數用來識別是串行隊列還是并發隊列。DISPATCH_QUEUE_SERIAL
表示串行隊列,DISPATCH_QUEUE_CONCURRENT
表示并發隊列。
// 串行隊列的創建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并發隊列的創建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.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_PRIORITY_DEFAULT
。第二個參數暫時沒用,用0
即可。
- 可以使用
// 全局并發隊列的獲取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
3.2 任務的創建方法
GCD 提供了同步執行任務的創建方法dispatch_sync
和異步執行任務創建方法dispatch_async
。
// 同步執行任務創建方法
dispatch_sync(queue, ^{
// 這里放同步執行任務代碼
});
// 異步執行任務創建方法
dispatch_async(queue, ^{
// 這里放異步執行任務代碼
});
雖然使用 GCD 只需兩步,但是既然我們有兩種隊列(串行隊列/并發隊列),兩種任務執行方式(同步執行/異步執行),那么我們就有了四種不同的組合方式。這四種不同的組合方式是:
- 同步執行 + 并發隊列
- 異步執行 + 并發隊列
- 同步執行 + 串行隊列
- 異步執行 + 串行隊列
實際上,剛才還說了兩種特殊隊列:全局并發隊列、主隊列。全局并發隊列可以作為普通并發隊列來使用。但是主隊列因為有點特殊,所以我們就又多了兩種組合方式。這樣就有六種不同的組合方式了。
- 同步執行 + 主隊列
- 異步執行 + 主隊列
那么這幾種不同組合方式各有什么區別呢,這里為了方便,先上結果,再來講解。你可以直接查看表格結果,然后跳過 4. GCD的基本使用 。
區別 | 并發隊列 | 串行隊列 | 主隊列 |
---|---|---|---|
同步(sync) | 沒有開啟新線程,串行執行任務 | 沒有開啟新線程,串行執行任務 | 主線程調用:死鎖卡住不執行 |
其他線程調用:沒有開啟新線程,串行執行任務 | |||
異步(async) | 有開啟新線程,并發執行任務 | 有開啟新線程(1條),串行執行任務 | 沒有開啟新線程,串行執行任務 |
下邊我們來分別講講這幾種不同的組合方式的使用方法。
4. GCD 的基本使用
先來講講并發隊列的兩種執行方式。
4.1 同步執行 + 并發隊列
- 在當前線程中執行任務,不會開啟新線程,執行完一個任務,再執行下一個任務。
/**
* 同步執行 + 并發隊列
* 特點:在當前線程中執行任務,不會開啟新線程,執行完一個任務,再執行下一個任務。
*/
- (void)syncConcurrent {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"syncConcurrent---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.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");
}
輸出結果如下:
2018-08-11 12:58:22.002801+0800 Demo[86020:3608082] currentThread---<NSThread: 0x604000263340>{number = 1, name = main}
2018-08-11 12:58:22.002950+0800 Demo[86020:3608082] syncConcurrent---begin
2018-08-11 12:58:24.004148+0800 Demo[86020:3608082] 1---<NSThread: 0x604000263340>{number = 1, name = main}
2018-08-11 12:58:26.004954+0800 Demo[86020:3608082] 1---<NSThread: 0x604000263340>{number = 1, name = main}
2018-08-11 12:58:28.005315+0800 Demo[86020:3608082] 2---<NSThread: 0x604000263340>{number = 1, name = main}
2018-08-11 12:58:30.005623+0800 Demo[86020:3608082] 2---<NSThread: 0x604000263340>{number = 1, name = main}
2018-08-11 12:58:32.006408+0800 Demo[86020:3608082] 3---<NSThread: 0x604000263340>{number = 1, name = main}
2018-08-11 12:58:34.006839+0800 Demo[86020:3608082] 3---<NSThread: 0x604000263340>{number = 1, name = main}
2018-08-11 12:58:34.007089+0800 Demo[86020:3608082] syncConcurrent---end
從同步執行 + 并發隊列
中可看到:
- 所有任務都是在當前線程(主線程)中執行的,沒有開啟新的線程(
同步執行
不具備開啟新線程的能力)。 - 所有任務都在打印的
syncConcurrent---begin
和syncConcurrent---end
之間執行的(同步任務
需要等待隊列的任務執行結束)。 - 任務按順序執行的。按順序執行的原因:雖然
并發隊列
可以開啟多個線程,并且同時執行多個任務。但是因為本身不能創建新線程,只有當前線程這一個線程(同步任務
不具備開啟新線程的能力),所以也就不存在并發。而且當前線程只有等待當前隊列中正在執行的任務執行完畢之后,才能繼續接著執行下面的操作(同步任務
需要等待隊列的任務執行結束)。所以任務只能一個接一個按順序執行,不能同時被執行。
4.2 異步執行 + 并發隊列
- 可以開啟多個線程,任務交替(同時)執行。
/**
* 異步執行 + 并發隊列
* 特點:可以開啟多個線程,任務交替(同時)執行。
*/
- (void)asyncConcurrent {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"asyncConcurrent---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.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");
}
輸出結果如下:
2018-08-11 13:02:19.331499+0800 Demo[86108:3611207] currentThread---<NSThread: 0x60400007b080>{number = 1, name = main}
2018-08-11 13:02:19.331649+0800 Demo[86108:3611207] asyncConcurrent---begin
2018-08-11 13:02:19.331755+0800 Demo[86108:3611207] asyncConcurrent---end
2018-08-11 13:02:21.334067+0800 Demo[86108:3611275] 1---<NSThread: 0x600000470d80>{number = 4, name = (null)}
2018-08-11 13:02:21.334067+0800 Demo[86108:3611271] 3---<NSThread: 0x600000466ec0>{number = 3, name = (null)}
2018-08-11 13:02:21.334079+0800 Demo[86108:3611276] 2---<NSThread: 0x60400026fa80>{number = 5, name = (null)}
2018-08-11 13:02:23.338321+0800 Demo[86108:3611275] 1---<NSThread: 0x600000470d80>{number = 4, name = (null)}
2018-08-11 13:02:23.338372+0800 Demo[86108:3611271] 3---<NSThread: 0x600000466ec0>{number = 3, name = (null)}
2018-08-11 13:02:23.338372+0800 Demo[86108:3611276] 2---<NSThread: 0x60400026fa80>{number = 5, name = (null)}
在異步執行 + 并發隊列
中可以看出:
- 除了當前線程(主線程),系統又開啟了3個線程,并且任務是交替/同時執行的。(
異步執行
具備開啟新線程的能力。且并發隊列
可開啟多個線程,同時執行多個任務)。 - 所有任務是在打印的
syncConcurrent---begin
和syncConcurrent---end
之后才執行的。說明當前線程沒有等待,而是直接開啟了新線程,在新線程中執行任務(異步執行
不做等待,可以繼續執行任務)。
接下來再來講講串行隊列的兩種執行方式。
4.3 同步執行 + 串行隊列
- 不會開啟新線程,在當前線程執行任務。任務是串行的,執行完一個任務,再執行下一個任務。
/**
* 同步執行 + 串行隊列
* 特點:不會開啟新線程,在當前線程執行任務。任務是串行的,執行完一個任務,再執行下一個任務。
*/
- (void)syncSerial {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"syncSerial---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.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");
}
輸出結果如下:
2018-08-11 13:04:54.852144+0800 Demo[86178:3613224] currentThread---<NSThread: 0x60400007c740>{number = 1, name = main}
2018-08-11 13:04:54.852279+0800 Demo[86178:3613224] syncSerial---begin
2018-08-11 13:04:56.852578+0800 Demo[86178:3613224] 1---<NSThread: 0x60400007c740>{number = 1, name = main}
2018-08-11 13:04:58.853066+0800 Demo[86178:3613224] 1---<NSThread: 0x60400007c740>{number = 1, name = main}
2018-08-11 13:05:00.853721+0800 Demo[86178:3613224] 2---<NSThread: 0x60400007c740>{number = 1, name = main}
2018-08-11 13:05:02.855192+0800 Demo[86178:3613224] 2---<NSThread: 0x60400007c740>{number = 1, name = main}
2018-08-11 13:05:04.855996+0800 Demo[86178:3613224] 3---<NSThread: 0x60400007c740>{number = 1, name = main}
2018-08-11 13:05:06.857636+0800 Demo[86178:3613224] 3---<NSThread: 0x60400007c740>{number = 1, name = main}
2018-08-11 13:05:06.858043+0800 Demo[86178:3613224] syncSerial---end
在同步執行 + 串行隊列
可以看到:
- 所有任務都是在當前線程(主線程)中執行的,并沒有開啟新的線程(
同步執行
不具備開啟新線程的能力)。 - 所有任務都在打印的
syncConcurrent---begin
和syncConcurrent---end
之間執行(同步任務
需要等待隊列的任務執行結束)。 - 任務是按順序執行的(
串行隊列
每次只有一個任務被執行,任務一個接一個按順序執行)。
4.4 異步執行 + 串行隊列
- 會開啟新線程,但是因為任務是串行的,執行完一個任務,再執行下一個任務
/**
* 異步執行 + 串行隊列
* 特點:會開啟新線程,但是因為任務是串行的,執行完一個任務,再執行下一個任務。
*/
- (void)asyncSerial {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"asyncSerial---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.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");
}
輸出結果如下:
2018-08-11 13:07:29.228955+0800 Demo[86241:3615363] currentThread---<NSThread: 0x6000000698c0>{number = 1, name = main}
2018-08-11 13:07:29.229071+0800 Demo[86241:3615363] asyncSerial---begin
2018-08-11 13:07:29.229180+0800 Demo[86241:3615363] asyncSerial---end
2018-08-11 13:07:31.232317+0800 Demo[86241:3615416] 1---<NSThread: 0x604000262100>{number = 3, name = (null)}
2018-08-11 13:07:33.236325+0800 Demo[86241:3615416] 1---<NSThread: 0x604000262100>{number = 3, name = (null)}
2018-08-11 13:07:35.241813+0800 Demo[86241:3615416] 2---<NSThread: 0x604000262100>{number = 3, name = (null)}
2018-08-11 13:07:37.242853+0800 Demo[86241:3615416] 2---<NSThread: 0x604000262100>{number = 3, name = (null)}
2018-08-11 13:07:39.245901+0800 Demo[86241:3615416] 3---<NSThread: 0x604000262100>{number = 3, name = (null)}
2018-08-11 13:07:41.251291+0800 Demo[86241:3615416] 3---<NSThread: 0x604000262100>{number = 3, name = (null)}
在異步執行 + 串行隊列
可以看到:
- 開啟了一條新線程(
異步執行
具備開啟新線程的能力,串行隊列
只開啟一個線程)。 - 所有任務是在打印的
syncConcurrent---begin
和syncConcurrent---end
之后才開始執行的(異步執行
不會做任何等待,可以繼續執行任務)。 - 任務是按順序執行的(
串行隊列
每次只有一個任務被執行,任務一個接一個按順序執行)。
下邊講講剛才我們提到過的特殊隊列:主隊列。
- 主隊列:GCD自帶的一種特殊的串行隊列
- 所有放在主隊列中的任務,都會放到主線程中執行
- 可使用
dispatch_get_main_queue()
獲得主隊列
我們再來看看主隊列的兩種組合方式。
4.5 同步執行 + 主隊列
同步執行 + 主隊列
在不同線程中調用結果也是不一樣,在主線程中調用會出現死鎖,而在其他線程中則不會。
4.5.1 在主線程中調用同步執行 + 主隊列
- 互相等待卡住不可行
/**
* 同步執行 + 主隊列
* 特點(主線程調用):互等卡主不執行。
* 特點(其他線程調用):不會開啟新線程,執行完一個任務,再執行下一個任務。
*/
- (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");
}
輸出結果如下:
2018-08-11 13:11:09.606001+0800 Demo[86328:3618363] currentThread---<NSThread: 0x60400007a800>{number = 1, name = main}
2018-08-11 13:11:09.606119+0800 Demo[86328:3618363] syncMain---begin
(lldb)
在同步執行 + 主隊列
可以驚奇的發現:
- 在主線程中使用
同步執行 + 主隊列
,追加到主線程的任務1、任務2、任務3都不再執行了,而且syncMain---end
也沒有打印,在XCode 9上還會報崩潰。這是為什么呢?
這是因為我們在主線程中執行syncMain
方法,相當于把syncMain
任務放到了主線程的隊列中。而同步執行
會等待當前隊列中的任務執行完畢,才會接著執行。那么當我們把任務1
追加到主隊列中,任務1
就在等待主線程處理完syncMain
任務。而syncMain
任務需要等待任務1
執行完畢,才能接著執行。
那么,現在的情況就是syncMain
任務和任務1
都在等對方執行完畢。這樣大家互相等待,所以就卡住了,所以我們的任務執行不了,而且syncMain---end
也沒有打印。
要是如果不在主線程中調用,而在其他線程中調用會如何呢?
4.5.2 在其他線程中調用同步執行 + 主隊列
- 不會開啟新線程,執行完一個任務,再執行下一個任務
// 使用 NSThread 的 detachNewThreadSelector 方法會創建線程,并自動啟動線程執行
// selector 任務
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
輸出結果如下:
2018-08-11 13:16:05.443988+0800 Demo[86419:3621778] currentThread---<NSThread: 0x600000262180>{number = 3, name = (null)}
2018-08-11 13:16:05.444135+0800 Demo[86419:3621778] syncMain---begin
2018-08-11 13:16:07.448102+0800 Demo[86419:3621701] 1---<NSThread: 0x604000064bc0>{number = 1, name = main}
2018-08-11 13:16:09.448885+0800 Demo[86419:3621701] 1---<NSThread: 0x604000064bc0>{number = 1, name = main}
2018-08-11 13:16:11.453896+0800 Demo[86419:3621701] 2---<NSThread: 0x604000064bc0>{number = 1, name = main}
2018-08-11 13:16:13.454217+0800 Demo[86419:3621701] 2---<NSThread: 0x604000064bc0>{number = 1, name = main}
2018-08-11 13:16:15.456579+0800 Demo[86419:3621701] 3---<NSThread: 0x604000064bc0>{number = 1, name = main}
2018-08-11 13:16:17.458279+0800 Demo[86419:3621701] 3---<NSThread: 0x604000064bc0>{number = 1, name = main}
2018-08-11 13:16:17.458926+0800 Demo[86419:3621778] syncMain---end
在其他線程中使用同步執行 + 主隊列
可看到:
- 所有任務都是在主線程(非當前線程)中執行的,沒有開啟新的線程(所有放在
主隊列
中的任務,都會放到主線程中執行)。 - 所有任務都在打印的
syncConcurrent---begin
和syncConcurrent---end
之間執行(同步任務
需要等待隊列的任務執行結束)。 - 任務是按順序執行的(主隊列是
串行隊列
,每次只有一個任務被執行,任務一個接一個按順序執行)。
為什么現在就不會卡住了呢?
因為syncMain 任務
放到了其他線程里,而任務1
、任務2
、任務3
都在追加到主隊列中,這三個任務都會在主線程中執行。syncMain 任務
在其他線程中執行到追加任務1
到主隊列中,因為主隊列現在沒有正在執行的任務,所以,會直接執行主隊列的任務1
,等任務1
執行完畢,再接著執行任務2
、任務3
。所以這里不會卡住線程。
4.6 異步執行 + 主隊列
- 只在主線程中執行任務,執行完一個任務,再執行下一個任務。
/**
* 異步執行 + 主隊列
* 特點:只在主線程中執行任務,執行完一個任務,再執行下一個任務
*/
- (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");
}
輸出結果如下:
2018-08-11 13:18:41.323423+0800 Demo[86477:3623633] currentThread---<NSThread: 0x600000261040>{number = 1, name = main}
2018-08-11 13:18:41.323545+0800 Demo[86477:3623633] asyncMain---begin
2018-08-11 13:18:41.323764+0800 Demo[86477:3623633] asyncMain---end
2018-08-11 13:18:43.328562+0800 Demo[86477:3623633] 1---<NSThread: 0x600000261040>{number = 1, name = main}
2018-08-11 13:18:45.329461+0800 Demo[86477:3623633] 1---<NSThread: 0x600000261040>{number = 1, name = main}
2018-08-11 13:18:47.330409+0800 Demo[86477:3623633] 2---<NSThread: 0x600000261040>{number = 1, name = main}
2018-08-11 13:18:49.330891+0800 Demo[86477:3623633] 2---<NSThread: 0x600000261040>{number = 1, name = main}
2018-08-11 13:18:51.331490+0800 Demo[86477:3623633] 3---<NSThread: 0x600000261040>{number = 1, name = main}
2018-08-11 13:18:53.332250+0800 Demo[86477:3623633] 3---<NSThread: 0x600000261040>{number = 1, name = main}
在異步執行 + 主隊列
可以看到:
- 所有任務都是在當前線程(主線程)中執行的,并沒有開啟新的線程(雖然
異步執行
具備開啟線程的能力,但因為是主隊列,所以所有任務都在主線程中)。 - 所有任務是在打印的syncConcurrent---begin和syncConcurrent---end之后才開始執行的(異步執行不會做任何等待,可以繼續執行任務)。
- 任務是按順序執行的(因為主隊列是
串行隊列
,每次只有一個任務被執行,任務一個接一個按順序執行)。
弄懂了難理解、繞來繞去的隊列+任務之后,我們來學習一個簡單的東西:5. GCD 線程間的通信。
5. GCD 線程間的通信
在iOS開發過程中,我們一般在主線程里邊進行UI刷新,例如:點擊、滾動、拖拽等事件。我們通常把一些耗時的操作放在其他線程,比如說圖片下載、文件上傳等耗時操作。而當我們有時候在其他線程完成了耗時操作時,需要回到主線程,那么就用到了線程之間的通訊。
/**
* 線程間通信
*/
- (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]); // 打印當前線程
});
});
}
輸出結果如下:
2018-08-11 13:22:20.202608+0800 Demo[86553:3626276] 1---<NSThread: 0x6040002736c0>{number = 3, name = (null)}
2018-08-11 13:22:22.207246+0800 Demo[86553:3626276] 1---<NSThread: 0x6040002736c0>{number = 3, name = (null)}
2018-08-11 13:22:24.208849+0800 Demo[86553:3626248] 2---<NSThread: 0x60000007d100>{number = 1, name = main}
- 可以看到在其他線程中先執行任務,執行完了之后回到主線程執行主線程的相應操作。
6. GCD 的其他方法
6.1 GCD 柵欄方法:dispatch_barrier_async
-
我們有時需要異步執行兩組操作,而且第一組操作執行完之后,才能開始執行第二組操作。這樣我們就需要一個相當于
柵欄
一樣的一個方法將兩組異步執行的操作組給分割起來,當然這里的操作組里可以包含一個或多個任務。這就需要用到dispatch_barrier_async
方法在兩個操作組間形成柵欄。
dispatch_barrier_async
函數會等待前邊追加到并發隊列中的任務全部執行完畢之后,再將指定的任務追加到該異步隊列中。然后在dispatch_barrier_async
函數追加的任務執行完畢之后,異步隊列才恢復為一般動作,接著追加任務到該異步隊列并開始執行。具體如下圖所示:[圖片上傳失敗...(image-ffd40e-1533963059124)]
/**
* 柵欄方法 dispatch_barrier_async
*/
- (void)barrier {
dispatch_queue_t queue = dispatch_queue_create("net.bujige.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_barrier_async(queue, ^{
// 追加任務 barrier
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印當前線程
}
});
dispatch_async(queue, ^{
// 追加任務3
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印當前線程
}
});
dispatch_async(queue, ^{
// 追加任務4
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印當前線程
}
});
}
輸出結果如下:
2018-08-11 13:23:43.943832+0800 Demo[86624:3628178] 2---<NSThread: 0x600000465280>{number = 4, name = (null)}
2018-08-11 13:23:43.943822+0800 Demo[86624:3628179] 1---<NSThread: 0x600000465400>{number = 3, name = (null)}
2018-08-11 13:23:45.946575+0800 Demo[86624:3628179] 1---<NSThread: 0x600000465400>{number = 3, name = (null)}
2018-08-11 13:23:45.946588+0800 Demo[86624:3628178] 2---<NSThread: 0x600000465280>{number = 4, name = (null)}
2018-08-11 13:23:47.950518+0800 Demo[86624:3628179] barrier---<NSThread: 0x600000465400>{number = 3, name = (null)}
2018-08-11 13:23:49.953835+0800 Demo[86624:3628179] barrier---<NSThread: 0x600000465400>{number = 3, name = (null)}
2018-08-11 13:23:51.957562+0800 Demo[86624:3628179] 3---<NSThread: 0x600000465400>{number = 3, name = (null)}
2018-08-11 13:23:51.957557+0800 Demo[86624:3628178] 4---<NSThread: 0x600000465280>{number = 4, name = (null)}
2018-08-11 13:23:53.962771+0800 Demo[86624:3628178] 4---<NSThread: 0x600000465280>{number = 4, name = (null)}
2018-08-11 13:23:53.962772+0800 Demo[86624:3628179] 3---<NSThread: 0x600000465400>{number = 3, name = (null)}
在dispatch_barrier_async
執行結果中可以看出:
- 在執行完柵欄前面的操作之后,才執行柵欄操作,最后再執行柵欄后邊的操作。
6.2 GCD 延時執行方法:dispatch_after
我們經常會遇到這樣的需求:在指定時間(例如3秒)之后執行某個任務??梢杂?GCD 的dispatch_after
函數來實現。
需要注意的是:dispatch_after
函數并不是在指定時間之后才開始執行處理,而是在指定時間之后將任務追加到主隊列中。嚴格來說,這個時間并不是絕對準確的,但想要大致延遲執行任務,dispatch_after
函數是很有效的。
/**
* 延時執行方法 dispatch_after
*/
- (void)after {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"asyncMain---begin");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2.0秒后異步追加任務代碼到主隊列,并開始執行
NSLog(@"after---%@",[NSThread currentThread]); // 打印當前線程
});
}
輸出結果如下:
2018-08-11 13:24:32.772460+0800 Demo[86674:3629363] currentThread---<NSThread: 0x604000072d40>{number = 1, name = main}
2018-08-11 13:24:32.772561+0800 Demo[86674:3629363] asyncMain---begin
2018-08-11 13:24:34.970205+0800 Demo[86674:3629363] after---<NSThread: 0x604000072d40>{number = 1, name = main}
可以看出:在打印 asyncMain---begin
之后大約 2.0 秒的時間,打印了 after---<NSThread: 0x60000006ee00>{number = 1, name = main}
6.3 GCD 一次性代碼(只執行一次):dispatch_once
- 我們在創建單例、或者有整個程序運行過程中只執行一次的代碼時,我們就用到了 GCD 的
dispatch_once
函數。使用
dispatch_once
函數能保證某段代碼在程序運行過程中只被執行1次,并且即使在多線程的環境下,dispatch_once
也可以保證線程安全。
/**
* 一次性代碼(只執行一次)dispatch_once
*/
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只執行1次的代碼(這里面默認是線程安全的)
});
}
6.4 GCD 快速迭代方法:dispatch_apply
- 通常我們會用 for 循環遍歷,但是 GCD 給我們提供了快速迭代的函數
dispatch_apply
。dispatch_apply
按照指定的次數將指定的任務追加到指定的隊列中,并等待全部隊列執行結束。
如果是在串行隊列中使用 dispatch_apply
,那么就和 for 循環一樣,按順序同步執行??蛇@樣就體現不出快速迭代的意義了。
我們可以利用并發隊列進行異步執行。比如說遍歷 0~5 這6個數字,for 循環的做法是每次取出一個元素,逐個遍歷。dispatch_apply
可以 在多個線程中同時(異步)遍歷多個數字。
還有一點,無論是在串行隊列,還是異步隊列中,dispatch_apply 都會等待全部任務執行完畢,這點就像是同步操作,也像是隊列組中的 dispatch_group_wait
方法。
/**
* 快速迭代方法 dispatch_apply
*/
- (void)apply {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"apply---begin");
dispatch_apply(6, queue, ^(size_t index) {
NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
}
輸出結果如下:
2018-08-11 13:27:16.110607+0800 Demo[86738:3631439] apply---begin
2018-08-11 13:27:16.111476+0800 Demo[86738:3631439] 0---<NSThread: 0x60000006de00>{number = 1, name = main}
2018-08-11 13:27:16.111487+0800 Demo[86738:3631497] 3---<NSThread: 0x604000276d00>{number = 5, name = (null)}
2018-08-11 13:27:16.111486+0800 Demo[86738:3631498] 1---<NSThread: 0x60000027cd40>{number = 3, name = (null)}
2018-08-11 13:27:16.111513+0800 Demo[86738:3631496] 2---<NSThread: 0x60000027ca40>{number = 4, name = (null)}
2018-08-11 13:27:16.111609+0800 Demo[86738:3631439] 4---<NSThread: 0x60000006de00>{number = 1, name = main}
2018-08-11 13:27:16.111617+0800 Demo[86738:3631497] 5---<NSThread: 0x604000276d00>{number = 5, name = (null)}
2018-08-11 13:27:16.111825+0800 Demo[86738:3631439] apply---end
因為是在并發隊列中異步執行任務,所以各個任務的執行時間長短不定,最后結束順序也不定。但是apply---end
一定在最后執行。這是因為dispatch_apply
函數會等待全部任務執行完畢。
6.5 GCD 隊列組:dispatch_group
有時候我們會有這樣的需求:分別異步執行2個耗時任務,然后當2個耗時任務都執行完畢后再回到主線程執行任務。這時候我們可以用到 GCD 的隊列組。
- 調用隊列組的
dispatch_group_async
先把任務放到隊列中,然后將隊列放入隊列組中?;蛘呤褂藐犃薪M的dispatch_group_enter、dispatch_group_leave
組合 來實現
dispatch_group_async
。 - 調用隊列組的
dispatch_group_notify
回到指定線程執行任務?;蛘呤褂?dispatch_group_wait
回到當前線程繼續向下執行(會阻塞當前線程)。
6.5.1 dispatch_group_notify
- 監聽 group 中任務的完成狀態,當所有的任務都執行完成后,追加任務到 group 中,并執行任務。
/**
* 隊列組 dispatch_group_notify
*/
- (void)groupNotify {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任務1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當前線程
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任務2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印當前線程
}
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的異步任務1、任務2都執行完畢后,回到主線程執行下邊任務
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印當前線程
}
NSLog(@"group---end");
});
}
輸出結果如下:
2018-08-11 13:28:07.481594+0800 Demo[86773:3632403] currentThread---<NSThread: 0x600000260a80>{number = 1, name = main}
2018-08-11 13:28:07.481747+0800 Demo[86773:3632403] group---begin
2018-08-11 13:28:09.487429+0800 Demo[86773:3632553] 2---<NSThread: 0x600000461c40>{number = 3, name = (null)}
2018-08-11 13:28:09.487440+0800 Demo[86773:3632555] 1---<NSThread: 0x60400046adc0>{number = 4, name = (null)}
2018-08-11 13:28:11.491841+0800 Demo[86773:3632555] 1---<NSThread: 0x60400046adc0>{number = 4, name = (null)}
2018-08-11 13:28:11.491866+0800 Demo[86773:3632553] 2---<NSThread: 0x600000461c40>{number = 3, name = (null)}
2018-08-11 13:28:13.493636+0800 Demo[86773:3632403] 3---<NSThread: 0x600000260a80>{number = 1, name = main}
2018-08-11 13:28:15.494681+0800 Demo[86773:3632403] 3---<NSThread: 0x600000260a80>{number = 1, name = main}
2018-08-11 13:28:15.495034+0800 Demo[86773:3632403] group---end
從dispatch_group_notify
相關代碼運行輸出結果可以看出:
當所有任務都執行完成之后,才執行dispatch_group_notify
block 中的任務。
6.5.2 dispatch_group_wait
- 暫停當前線程(阻塞當前線程),等待指定的 group 中的任務執行完成后,才會往下繼續執行。
/**
* 隊列組 dispatch_group_wait
*/
- (void)groupWait {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任務1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當前線程
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任務2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印當前線程
}
});
// 等待上面的任務全部完成后,會往下繼續執行(會阻塞當前線程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"group---end");
}
輸出結果如下:
2018-08-11 13:29:11.640449+0800 Demo[86816:3633678] currentThread---<NSThread: 0x60000006d7c0>{number = 1, name = main}
2018-08-11 13:29:11.640565+0800 Demo[86816:3633678] group---begin
2018-08-11 13:29:13.646119+0800 Demo[86816:3633725] 2---<NSThread: 0x60400026d740>{number = 4, name = (null)}
2018-08-11 13:29:13.646132+0800 Demo[86816:3633724] 1---<NSThread: 0x600000461f40>{number = 3, name = (null)}
2018-08-11 13:29:15.647119+0800 Demo[86816:3633725] 2---<NSThread: 0x60400026d740>{number = 4, name = (null)}
2018-08-11 13:29:15.647732+0800 Demo[86816:3633724] 1---<NSThread: 0x600000461f40>{number = 3, name = (null)}
2018-08-11 13:29:15.648183+0800 Demo[86816:3633678] group---end
從dispatch_group_wait
相關代碼運行輸出結果可以看出:
當所有任務執行完成之后,才執行 dispatch_group_wait
之后的操作。但是,使用dispatch_group_wait
會阻塞當前線程。
6.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
中的任務。
/**
* 隊列組 dispatch_group_enter、dispatch_group_leave
*/
- (void)groupEnterAndLeave
{
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 追加任務1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當前線程
}
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 追加任務2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印當前線程
}
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的異步操作都執行完畢后,回到主線程.
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印當前線程
}
NSLog(@"group---end");
});
// // 等待上面的任務全部完成后,會往下繼續執行(會阻塞當前線程)
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//
// NSLog(@"group---end");
}
輸出結果如下:
2018-08-11 13:30:27.252586+0800 Demo[86866:3635097] currentThread---<NSThread: 0x6040002629c0>{number = 1, name = main}
2018-08-11 13:30:27.252699+0800 Demo[86866:3635097] group---begin
2018-08-11 13:30:29.257183+0800 Demo[86866:3635155] 2---<NSThread: 0x604000474f40>{number = 3, name = (null)}
2018-08-11 13:30:29.257184+0800 Demo[86866:3635153] 1---<NSThread: 0x604000475180>{number = 4, name = (null)}
2018-08-11 13:30:31.261655+0800 Demo[86866:3635153] 1---<NSThread: 0x604000475180>{number = 4, name = (null)}
2018-08-11 13:30:31.261659+0800 Demo[86866:3635155] 2---<NSThread: 0x604000474f40>{number = 3, name = (null)}
2018-08-11 13:30:33.263230+0800 Demo[86866:3635097] 3---<NSThread: 0x6040002629c0>{number = 1, name = main}
2018-08-11 13:30:35.264518+0800 Demo[86866:3635097] 3---<NSThread: 0x6040002629c0>{number = 1, name = main}
2018-08-11 13:30:35.264915+0800 Demo[86866:3635097] group---end
從dispatch_group_enter、dispatch_group_leave
相關代碼運行結果中可以看出:當所有任務執行完成之后,才執行 dispatch_group_notify 中的任務。這里的dispatch_group_enter、dispatch_group_leave
組合,其實等同于dispatch_group_async
。
6.6 GCD 信號量:dispatch_semaphore
GCD 中的信號量是指 Dispatch Semaphore,是持有計數的信號。類似于過高速路收費站的欄桿??梢酝ㄟ^時,打開欄桿,不可以通過時,關閉欄桿。在 Dispatch Semaphore 中,使用計數來完成這個功能,計數為0時等待,不可通過。計數為1或大于1時,計數減1且不等待,可通過。
Dispatch Semaphore 提供了三個函數。
-
dispatch_semaphore_create
:創建一個Semaphore并初始化信號的總量 -
dispatch_semaphore_signal
:發送一個信號,讓信號總量加1 -
dispatch_semaphore_wait
:可以使總信號量減1,當信號總量為0時就會一直等待(阻塞所在線程),否則就可以正常執行。
注意:信號量的使用前提是:想清楚你需要處理哪個線程等待(阻塞),又要哪個線程繼續執行,然后使用信號量。
Dispatch Semaphore 在實際開發中主要用于:
- 保持線程同步,將異步執行任務轉換為同步執行任務
- 保證線程安全,為線程加鎖
6.6.1 Dispatch Semaphore 線程同步
我們在開發中,會遇到這樣的需求:異步執行耗時任務,并使用異步執行的結果進行一些額外的操作。換句話說,相當于,將將異步執行任務轉換為同步執行任務。比如說:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath:
方法。通過引入信號量的方式,等待異步執行任務結果,獲取到 tasks,然后再返回該 tasks。
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
下面,我們來利用 Dispatch Semaphore 實現線程同步,將異步執行任務轉換為同步執行任務。
/**
* semaphore 線程同步
*/
- (void)semaphoreSync {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"semaphore---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
// 追加任務1
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當前線程
number = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
}
輸出結果如下:
2018-08-11 13:31:29.562640+0800 Demo[86917:3636385] currentThread---<NSThread: 0x604000065a80>{number = 1, name = main}
2018-08-11 13:31:29.562761+0800 Demo[86917:3636385] semaphore---begin
2018-08-11 13:31:31.564476+0800 Demo[86917:3636443] 1---<NSThread: 0x604000261740>{number = 3, name = (null)}
2018-08-11 13:31:31.564882+0800 Demo[86917:3636385] semaphore---end,number = 100
從 Dispatch Semaphore 實現線程同步的代碼可以看到:
-
semaphore---end
是在執行完number = 100;
之后才打印的。而且輸出結果 number 為 100。
這是因為異步執行
不會做任何等待,可以繼續執行任務。異步執行
將任務1追加到隊列之后,不做等待,接著執行dispatch_semaphore_wait
方法。此時 semaphore == 0,當前線程進入等待狀態。然后,異步任務1開始執行。任務1執行到dispatch_semaphore_signal
之后,總信號量,此時 semaphore == 1,dispatch_semaphore_wait
方法使總信號量減1,正在被阻塞的線程(主線程)恢復繼續執行。最后打印semaphore---end,number = 100
。這樣就實現了線程同步,將異步執行任務轉換為同步執行任務。
6.6.2 Dispatch Semaphore 線程安全和線程同步(為線程加鎖)
線程安全:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作(更改變量),一般都需要考慮線程同步,否則的話就可能影響線程安全。
線程同步:可理解為線程 A 和 線程 B 一塊配合,A 執行到一定程度時要依靠線程 B 的某個結果,于是停下來,示意 B 運行;B 依言執行,再將結果給 A;A 再繼續操作。
舉個簡單例子就是:兩個人在一起聊天。兩個人不能同時說話,避免聽不清(操作沖突)。等一個人說完(一個線程結束操作),另一個再說(另一個線程再開始操作)。
下面,我們模擬火車票售賣的方式,實現 NSThread 線程安全和解決線程同步問題。
場景:總共有50張火車票,有兩個售賣火車票的窗口,一個是北京火車票售賣窗口,另一個是上?;疖嚻笔圪u窗口。兩個窗口同時售賣火車票,賣完為止。
6.6.2.1 非線程安全(不使用 semaphore)
先來看看不考慮線程安全的代碼:
/**
* 非線程安全:不使用 semaphore
* 初始化火車票數量、賣票窗口(非線程安全)、并開始賣票
*/
- (void)initTicketStatusNotSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"semaphore---begin");
self.ticketSurplusCount = 50;
// queue1 代表北京火車票售賣窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上?;疖嚻笔圪u窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketNotSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketNotSafe];
});
}
/**
* 售賣火車票(非線程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
if (self.ticketSurplusCount > 0) { //如果還有票,繼續售賣
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票數:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { //如果已賣完,關閉售票窗口
NSLog(@"所有火車票均已售完");
break;
}
}
}
輸出結果部分如下:
2018-08-11 13:37:17.857271+0800 Demo[87115:3642184] currentThread---<NSThread: 0x600000075b00>{number = 1, name = main}
2018-08-11 13:37:17.857392+0800 Demo[87115:3642184] semaphore---begin
2018-08-11 13:37:17.857580+0800 Demo[87115:3642254] 剩余票數:49 窗口:<NSThread: 0x60000027d040>{number = 3, name = (null)}
...
2018-08-11 13:37:18.466179+0800 Demo[87115:3642252] 剩余票數:42 窗口:<NSThread: 0x60000027d180>{number = 4, name = (null)}
2018-08-11 13:37:18.671515+0800 Demo[87115:3642252] 剩余票數:40 窗口:<NSThread: 0x60000027d180>{number = 4, name = (null)}
2018-08-11 13:37:18.671534+0800 Demo[87115:3642254] 剩余票數:41 窗口:<NSThread: 0x60000027d180>{number = 4, name = (null)}
...
2018-08-11 13:37:22.743057+0800 Demo[87115:3642252] 剩余票數:3 窗口:<NSThread: 0x60000027d180>{number = 4, name = (null)}
2018-08-11 13:37:22.743057+0800 Demo[87115:3642254] 剩余票數:4 窗口:<NSThread: 0x60000027d040>{number = 3, name = (null)}
2018-08-11 13:37:22.945353+0800 Demo[87115:3642252] 剩余票數:2 窗口:<NSThread: 0x60000027d180>{number = 4, name = (null)}
2018-08-11 13:37:22.945395+0800 Demo[87115:3642254] 剩余票數:1 窗口:<NSThread: 0x60000027d040>{number = 3, name = (null)}
2018-08-11 13:37:23.150096+0800 Demo[87115:3642254] 所有火車票均已售完
2018-08-11 13:37:23.150134+0800 Demo[87115:3642252] 剩余票數:0 窗口:<NSThread: 0x60000027d180>{number = 4, name = (null)}
2018-08-11 13:37:23.351216+0800 Demo[87115:3642252] 所有火車票均已售完
可以看到在不考慮線程安全,不使用 semaphore 的情況下,得到票數是錯亂的,這樣顯然不符合我們的需求,所以我們需要考慮線程安全問題。
6.6.2.2 線程安全(使用 semaphore 加鎖)
考慮線程安全的代碼:
/**
* 線程安全:使用 semaphore 加鎖
* 初始化火車票數量、賣票窗口(線程安全)、并開始賣票
*/
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
NSLog(@"semaphore---begin");
semaphoreLock = dispatch_semaphore_create(1);
self.ticketSurplusCount = 50;
// queue1 代表北京火車票售賣窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上?;疖嚻笔圪u窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketSafe];
});
}
/**
* 售賣火車票(線程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 相當于加鎖
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { //如果還有票,繼續售賣
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票數:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { //如果已賣完,關閉售票窗口
NSLog(@"所有火車票均已售完");
// 相當于解鎖
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 相當于解鎖
dispatch_semaphore_signal(semaphoreLock);
}
}
輸出結果部分如下:
2018-08-11 13:42:04.120736+0800 Demo[87233:3646094] currentThread---<NSThread: 0x60000006ba00>{number = 1, name = main}
2018-08-11 13:42:04.120860+0800 Demo[87233:3646094] semaphore---begin
2018-08-11 13:42:04.121626+0800 Demo[87233:3646147] 剩余票數:49 窗口:<NSThread: 0x6000002739c0>{number = 3, name = (null)}
2018-08-11 13:42:04.324580+0800 Demo[87233:3646153] 剩余票數:48 窗口:<NSThread: 0x6000002724c0>{number = 4, name = (null)}
2018-08-11 13:42:04.528595+0800 Demo[87233:3646147] 剩余票數:47 窗口:<NSThread: 0x6000002739c0>{number = 3, name = (null)}
2018-08-11 13:42:04.731887+0800 Demo[87233:3646153] 剩余票數:46 窗口:<NSThread: 0x6000002724c0>{number = 4, name = (null)}
2018-08-11 13:42:04.935620+0800 Demo[87233:3646147] 剩余票數:45 窗口:<NSThread: 0x6000002739c0>{number = 3, name = (null)}
...
2018-08-11 13:42:13.065732+0800 Demo[87233:3646147] 剩余票數:5 窗口:<NSThread: 0x6000002739c0>{number = 3, name = (null)}
2018-08-11 13:42:13.270480+0800 Demo[87233:3646153] 剩余票數:4 窗口:<NSThread: 0x6000002724c0>{number = 4, name = (null)}
2018-08-11 13:42:13.472125+0800 Demo[87233:3646147] 剩余票數:3 窗口:<NSThread: 0x6000002739c0>{number = 3, name = (null)}
2018-08-11 13:42:13.674344+0800 Demo[87233:3646153] 剩余票數:2 窗口:<NSThread: 0x6000002724c0>{number = 4, name = (null)}
2018-08-11 13:42:13.876214+0800 Demo[87233:3646147] 剩余票數:1 窗口:<NSThread: 0x6000002739c0>{number = 3, name = (null)}
2018-08-11 13:42:14.078281+0800 Demo[87233:3646153] 剩余票數:0 窗口:<NSThread: 0x6000002724c0>{number = 4, name = (null)}
2018-08-11 13:42:14.280213+0800 Demo[87233:3646147] 所有火車票均已售完
2018-08-11 13:42:14.280628+0800 Demo[87233:3646153] 所有火車票均已售完
可以看出,在考慮了線程安全的情況下,使用 dispatch_semaphore
機制之后,得到的票數是正確的,沒有出現混亂的情況。我們也就解決了多個線程同步的問題。
參考資料:
iOS多線程詳盡總結系列文章: