這是 Objective-C 高級(jí)編程 第三章,終于在拖了快一個(gè)月把這本書完結(jié)了。最近都比較忙啊,有時(shí)間的時(shí)候都去熟悉公司的代碼了,其他學(xué)習(xí)的時(shí)間就減少了。開始正題吧,GCD 是異步執(zhí)行任務(wù)的技術(shù)之一,開發(fā)者使用 GCD 來開發(fā)多線程程序。首先回顧下蘋果官方對(duì) GCD 的說明:開發(fā)者要做的只是定義想執(zhí)行的任務(wù)并將其追加到適當(dāng)?shù)?Dispatch Queue 中。
書中對(duì)線程有一個(gè)定義是:線程是 1 個(gè) CPU 執(zhí)行的 CPU 命令列為一條無分叉路徑。現(xiàn)在一個(gè)物理的 CPU 芯片實(shí)際上有 64 個(gè)(64 核)CPU,如果一個(gè) CPU 核虛擬為兩個(gè) CPU 工作,那么一臺(tái)計(jì)算機(jī)上使用多個(gè) CPU 核就是理所當(dāng)然的事了。盡管如此,“一個(gè) CPU 核執(zhí)行的 CPU 命令列為一條無分叉路徑” 仍然不變。
為什么無論是桌面應(yīng)用、Android、還是 iOS,開發(fā)一個(gè) App 都需要如此重視多線程呢?因?yàn)槭褂枚嗑€程編程可以保證應(yīng)用程序的響應(yīng)性能。App 都有一個(gè)用來描繪用戶界面,處理觸摸屏幕的事件的線程稱為 主線程。如果阻塞了 主線程 的執(zhí)行,就會(huì)導(dǎo)致不能更新用戶界面、應(yīng)用程序的畫面長(zhǎng)時(shí)間停滯等問題,用戶就會(huì)以為 死機(jī) 了。 使用多線程編程,在執(zhí)行長(zhǎng)時(shí)間的處理時(shí)仍能保證用戶界面的響應(yīng)性能。
總結(jié)
GCD 中 Block
是同步執(zhí)行還是異步執(zhí)行取決于 Block
所屬的 Dispatch Queue
是 Serial
還是 Concurrent
。當(dāng)前線程是否被阻塞是由調(diào)用的函數(shù)名決定的,比如:dispatch_async
函數(shù)不會(huì)阻塞當(dāng)前線程,而 dispatch_sync
, dispatch_group_wait
則會(huì)阻塞當(dāng)前線程。
Dispatch Queue
最簡(jiǎn)單的 GCD 代碼格式為:
dispatch_async(queue, ^{
// 想要執(zhí)行的任務(wù)
});
僅這樣就可使指定的 Block 在另一線程中執(zhí)行。
Dispatch Queue
是一個(gè)執(zhí)行處理的 先入先出(FIFO) 等待隊(duì)列。開發(fā)者通過 dispatch_async
函數(shù)等 API,將指定的 Block
封裝后追加到指定的 Dispatch Queue
中。
Serial Dispatch Queue 和 Concurrent Dispatch Queue
Dispatch Queue 的種類 | 說明 |
---|---|
Serial Dispatch Queue | 等待正在執(zhí)行中的處理結(jié)束(同步執(zhí)行) |
Concurrent Dispatch Queue | 不等待正在執(zhí)行中的處理結(jié)束(異步執(zhí)行) |
無論是
Serial
還是Concurrent
的Dispatch Queue
,它們都是 FIFO 的隊(duì)列,所以先加入隊(duì)列的處理一定會(huì)先出隊(duì),出隊(duì)后能夠占領(lǐng)一個(gè)線程,但它占領(lǐng)的線程不一定會(huì)先執(zhí)行。
dispatch_queue_create
// 創(chuàng)建一個(gè) Serial Dispatch Queue
dispatch_queue_t serialQueue = dispatch_queue_create("yogy.jianshu.gcd.serialQueue", NULL);
// 創(chuàng)建一個(gè) Concurrent Dispatch Queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("yogy.jianshu.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_create
函數(shù)的第一個(gè)參數(shù)是 Dispatch Queue
的名稱,推薦使用逆序全稱域名(FQDN),該名稱會(huì)出現(xiàn)在在應(yīng)用程序崩潰時(shí)的 CrashLog 中。
雖然一個(gè)
Serial Dispatch Queue
中的處理是串行執(zhí)行的,但多個(gè)Serial Dispatch Queue
之間是并行執(zhí)行的。一旦生成Serial Dispatch Queue
并追加處理,系統(tǒng)對(duì)于一個(gè)Serial Dispatch Queue
就只生成并使用一個(gè)線程。如果生成 2000 個(gè)Serial Dispatch Queue
,那么就生成了 2000 個(gè)線程。而對(duì)于Concurrent Dispatch Queue
來說,不管生成多少,由于 XNU 只使用有效管理的線程,因此不會(huì)發(fā)生Serial Dispatch Queue
那樣的問題。雖然Serial Dispatch Queue
比Concurrent Dispatch Queue
能生成更多的線程,但絕不能激動(dòng)之下大量生成Serial Dispatch Queue
。
dispatch_release
在 ARC 情況下,已經(jīng)不用它了。
所有通過類似 dispatch_xxxxx_create
函數(shù)創(chuàng)建的 GCD 對(duì)象,都需要通過 dispatch_release
函數(shù)釋放。
dispatch_queue_t serialQueue = dispatch_queue_create("yogy.jianshu.gcd.serialQueue", NULL);
dispatch_async(serialQueue, ^{
NSLog(@"www.jianshu.yogy");
});
dispatch_release(serialQueue);
因?yàn)槊恳粋€(gè)加入 Dispatch Queue
中的 Block
都會(huì)被封裝且持有其加入的 Dispatch Queue
對(duì)象,所以,當(dāng)含有 create
的 API 生成的對(duì)象不需要的時(shí)候有必要通過 dispatch_release
函數(shù)進(jìn)行釋放。在通過函數(shù)或方法獲取 Dispatch Queue
以及其他名稱中含有 create
的 API 生成的對(duì)象時(shí),有必要通過 dispatch_retain
函數(shù)持有,并在不需要的時(shí)候通過 dispatch_release
釋放。
Main Dispatch Queue/Global Dispatch Queue
開發(fā)者除了使用 dispatch_queue_create
生成 Dispatch Queue
外,還能獲取系統(tǒng)標(biāo)準(zhǔn)提供的 Dispatch Queue
。
Main Dispatch Queue
是 App 的主線程,它是一個(gè) Serial Dispatch Queue
。追加到 Main Dispatch Queue
的處理在主線程的 RunLoop 中執(zhí)行。由于在主線程中執(zhí)行,因此要將用戶界面的界面更新等一些必須在主線程中執(zhí)行的處理追加到 Main Dispatch Queue
中。
Global Dispatch Queue
是所有應(yīng)用程序都能夠使用的 Concurrent Dispatch Queue
。Global Dispatch Queue
有四種優(yōu)先級(jí),但是通過 XNU 內(nèi)核用于 Global Dispatch Queue
的線程并不能保證實(shí)時(shí)性,因此執(zhí)行優(yōu)先級(jí)只是大致的判斷。例如在處理內(nèi)容可有可無時(shí),使用后臺(tái)優(yōu)先級(jí)的 Global Dispatch Queue
等,只能進(jìn)行這種程度的區(qū)分。
// 獲取 Main Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 獲取高優(yōu)先級(jí)的 Global Dispatch Queue
dispatch_queue_t globalQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 獲取其他優(yōu)先級(jí)的 Global Dispatch Queue,只修改第一個(gè)參數(shù)即可。優(yōu)先級(jí)有:HIGH, DEFAULT, LOW, BACKGROUND
對(duì)于
Main Dispatch Queue
和Global Dispatch Queue
執(zhí)行dispatch_retain
和dispatch_release
函數(shù)不會(huì)引發(fā)任何問題。
使用 Main Dispatch Queue
和 Global Dispatch Queue
的例子:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 這里進(jìn)行可并行執(zhí)行的處理、比較耗時(shí)的處理、占用資源的處理
dispatch_async(dispatch_get_main_queue(), ^{
// 這里進(jìn)行必須在主線程執(zhí)行的處理,如更新 UI
});
});
dispatch_set_target_queue
通過 dispatch_queue_create
函數(shù)生成的 Dispatch Queue
不管是 Serial Dispatch Queue
還是 Concurrent Dispatch Queue
,都使用與默認(rèn)優(yōu)先級(jí)的 Global Dispatch Queue
相同執(zhí)行優(yōu)先級(jí)的線程。而變更生成的 Dispatch Queue
的執(zhí)行優(yōu)先級(jí)要使用 dispatch_set_target_queue
函數(shù)。如以下代碼:
dispatch_queue_t serialQueue = dispatch_queue_create("yogy.jianshu.gcd.serialQueue", NULL);
dispatch_queue_t globalQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(serialQueue, globalQueueBackground);
// 這個(gè)函數(shù)可以理解為:將 serialQueue 中的處理取出來再加入到 globalQueueBackground 中。
dispatch_set_target_queue
函數(shù)的第一個(gè)參數(shù)不可為:Main Dispatch Queue
和Global Dispatch Queue
,否則屬于未定義行為。當(dāng)在必須不可并行執(zhí)行的處理追加到多個(gè)Serial Dispatch Queue
中時(shí),使用dispatch_set_target_queue
函數(shù)將多個(gè)Serial Dispatch Queue
的目標(biāo)指定為同一個(gè)Serial Dispatch Queue
,即可防止處理并行執(zhí)行。
dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"www.jianshu.yogy");
});
3ull * NSEC_PER_SEC
指 3s,150ull *NSEC_PER_MSEC
指 150ms,都是按 PER 后面的單詞為單位。
該代碼并不是指 Block 將在 3s 后被執(zhí)行,而是指 3s 后才會(huì)把 Block 追加到
Main Dispatch Queue
中(類似于dispatch_async
函數(shù))。所以如果 RunLoop 每隔 1/60s 執(zhí)行一次,Block 最快在 3s 后執(zhí)行,最慢在 (3+1/60)s 執(zhí)行。
從 NSDate
得到 dispatch_time_t
:
dispatch_time_t getDispatchTimeByDate(NSDate date) {
NSTimeInterval interval;
double second, subsecond;
struct timespec time;
dispatch_time_t milestone;
interval = [date timeIntervalSince1970];
subsecond = modf(interval, &second);
time.tv_sec = second;
time.tv_nsec = subsecond * NSEC_PER_SEC;
milestone = dispatch_walltime(&time, 0);
return milestone;
}
Dispatch Group
當(dāng)需要在某一組的多個(gè)處理全部執(zhí)行完成后再執(zhí)行其他處理,使用 Dispatch Group
。Dispatch Group
是針對(duì) Block
而言的,并不是針對(duì) Dispatch Queue
的,這很重要。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t q1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t q2 = dispatch_queue_create("yogy.jianshu.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t gp = dispatch_group_create();
dispatch_group_async(gp, q1, ^{ NSLog(@"www.jianshu.yogy -> q1 . 1"); });
dispatch_group_async(gp, q1, ^{ NSLog(@"www.jianshu.yogy -> q1 . 2"); });
dispatch_group_async(gp, q2, ^{ NSLog(@"www.jianshu.yogy -> q2 . 1"); });
dispatch_group_async(gp, q2, ^{ NSLog(@"www.jianshu.yogy -> q2 . 2"); });
dispatch_group_notify(gp, dispatch_get_main_queue(), ^{ NSLog(@"www.jianshu.yogy -> done"); });
// 輸出為:
// www.jianshu.yogy -> q2 . 2
// www.jianshu.yogy -> q2 . 1
// www.jianshu.yogy -> q1 . 1
// www.jianshu.yogy -> q1 . 2
// www.jianshu.yogy -> done
// 前四個(gè)輸出的順序不一定,但 done 總是會(huì)最后輸出。
}
使用 dispatch_group_notify
方法,一旦檢測(cè)到所有加入到 Group 中的處理執(zhí)行結(jié)束后,就將結(jié)束處理追加到指定的 Dispatch Queue
中。
另外,在 Dispatch Group
中也可以使用 dispatch_group_wait
函數(shù)僅等待全部處理執(zhí)行結(jié)束。
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 會(huì)一直阻塞當(dāng)前線程直到 group 中的所有處理全部執(zhí)行完成。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
// 返回 0,表示 group 中的全部處理都執(zhí)行結(jié)束
} else {
// 否則,表示 group 中的處理為執(zhí)行結(jié)束
}
dispatch_group_wait
會(huì)阻塞當(dāng)前線程是指,一旦調(diào)用了dispatch_group_wait
函數(shù),該函數(shù)就處于調(diào)用狀態(tài)而不返回。即執(zhí)行dispatch_group_wait
函數(shù)的線程停止,直到經(jīng)過了dispatch_group_wait
函數(shù)中指定的時(shí)間或?qū)儆谥付?Dispatch Group
的處理全部執(zhí)行結(jié)束。
dispatch_barrier_async
該函數(shù)必須同 dispatch_queue_create
函數(shù)生成的 Concurrent Dispatch Queue
一起使用。同 dispatch_get_global_queue
得到的 Concurrent Dispatch Queue
一起使用不會(huì)生效。
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_async
函數(shù)會(huì)等待當(dāng)前的 queue 中已有的處理全部執(zhí)行完后再添加 blk_for_writing,并且還保證不會(huì)添加其他的處理,只有當(dāng) blk_for_writing 執(zhí)行完后才會(huì)恢復(fù)為一般的 Concurrent Dispatch Queue
,后面的處理又可以并行執(zhí)行了。
dispatch_sync
dispatch_sync
函數(shù)會(huì)阻塞當(dāng)前進(jìn)程直到指定的 Block 被執(zhí)行完成。它使當(dāng)前線程進(jìn)入該函數(shù),但是要等到指定的 Block 被執(zhí)行完才返回。該方法容易引發(fā)死鎖。
dispatch_queue_t queue = dispatch_queue_create("yogy.jianshu.gcd", NULL);
dispatch_async(queue, ^{
NSLog(@"enter...");
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:3.0];
NSLog(@"www.jianshu.yogy");
});
NSLog(@"exit...");
});
// 會(huì)發(fā)生死鎖,輸出只有
// enter...
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"enter...");
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:3.0];
NSLog(@"www.jianshu.yogy");
});
NSLog(@"exit...");
});
// 不會(huì)出現(xiàn)死鎖,輸出為:
// enter...
// www.jianshu.yogy
// exit...
從上面兩個(gè)例子看出:dispatch_sync
容易對(duì) Serial Dispatch Queue
引起死鎖。解析一下引起死鎖的原因。對(duì)于 queue 為 Serial Dispatch Queue
的時(shí)候,使用 dispatch_sync(queue, ^{})
已經(jīng)把 Block 加入到 queue 中,但是由于 queue 是串行執(zhí)行的隊(duì)列且當(dāng)前正在執(zhí)行中,所以加入的 Block 沒有機(jī)會(huì)執(zhí)行,而當(dāng)前線程又再等待 Block 執(zhí)行結(jié)束。這樣就發(fā)生死鎖了。而對(duì)于 queue 為 Concurrent Dispatch Queue
的時(shí)候,使用 dispatch_sync(queue, ^{})
同樣把 Block 加入到了 queue 中,但不同的是 queue 可并行執(zhí)行,也就是 Block 可以被加入到其他的線程中執(zhí)行,于是等 Block 在其他線程執(zhí)行完后,當(dāng)前線程就可以繼續(xù)執(zhí)行了。但是當(dāng) queue 為 Concurrent Dispatch Queue
時(shí)還是存在潛在問題的,比如當(dāng)前的所有線程都處于等待狀態(tài)的話,同樣就沒有 Block 能執(zhí)行了。如下面的代碼:
static long cnt = 0;
dispatch_queue_t queue = dispatch_queue_create("yogy.jianshu.gcd", DISPATCH_QUEUE_CONCURRENT);
// 如果使用系統(tǒng)的 Global Dispatch Queue 就不會(huì)死鎖,應(yīng)該是系統(tǒng)內(nèi)部實(shí)現(xiàn)不同。
// 如果不注釋下面一行,不會(huì)出現(xiàn)死鎖。
// queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 1000000; ++i) {
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:5.0];
dispatch_sync(queue, ^{
NSLog(@"%ld", ++cnt);
});
});
}
NSLog(@"www.jianshu.yogy");
dispatch_apply
dispatch_apply
也會(huì)阻塞當(dāng)前線程,直到指定的 Block 執(zhí)行完指定的次數(shù)。書中說:dispatch_apply
是 dispatch_sync
和 Dispatch Group 的關(guān)聯(lián) API,不知道怎么解釋哈。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu", index);
});
NSLog(@"www.jianshu.yogy");
// 下標(biāo)的輸出順序不定,但是 www.jianshu.yogy 一定會(huì)最后輸出。
由于
dispatch_apply
函數(shù)也與dispatch_sync
函數(shù)相同,會(huì)等待處理執(zhí)行結(jié)束,因此推薦在dispatch_async
函數(shù)中非同步執(zhí)行dispatch_apply
函數(shù)。
dispatch_suspend / dispatch_resume
參數(shù)為一個(gè)隊(duì)列。這些函數(shù)對(duì)已經(jīng)在執(zhí)行的處理沒有影響。掛起后,追加到 Dispatch Queue
中但尚未執(zhí)行的處理在此之后停止執(zhí)行。而恢復(fù)則使得這些處理能夠繼續(xù)執(zhí)行。
dispatch_semaphore
這玩意能在多線程之間進(jìn)行通信,一般我們認(rèn)為某一個(gè)處理不能和另一個(gè)處理同時(shí)進(jìn)行,但我們都是相對(duì)于整個(gè)處理而言的。可是如果某一個(gè)處理的某一段代碼不能與另一個(gè)處理的某一段代碼并行執(zhí)行,而其他地方又可以并行執(zhí)行呢?這時(shí)候,dispatch_semaphore
就派上用場(chǎng)呢。
// 創(chuàng)建一個(gè)計(jì)數(shù)值為 1 的 dispatch_semaphore_t
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
// 等待 semaphore 的計(jì)數(shù)值 >= 1,并將計(jì)數(shù)值減 1 后返回,會(huì)阻塞當(dāng)前線程。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_semaphore_wait(semaphore, time);
// 等待 time 時(shí)間或 semaphore 的計(jì)數(shù)值 >= 1 返回。
// 返回值為 0,則等待到了有效信號(hào)
// 否則,只是達(dá)到了 time 時(shí)間,但并沒有得到有效信號(hào)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 10; ++i) {
dispatch_async(queue, ^{
// 一直等到有效信號(hào),并將計(jì)數(shù)值減 1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[array addObject:[NSNumber numberWithInt:i]];
NSLog(@"%d", i); // 0-9 依次輸出
// 發(fā)出信號(hào)后,會(huì)按 dispatch_semaphore_wait 的等待順序依次執(zhí)行,F(xiàn)IFO。
dispatch_semaphore_signal(semaphore);
});
}
dispatch_once
常用于單例模式,對(duì)于同一個(gè) dispatch_once_t,程序只會(huì)跑一次。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只被執(zhí)行依次的代碼
});