Grand Central Dispatch
3.1 Grand Central Dispatch(GCD)摘要
3.1.1 什么是GCD
Grand Central Dispatch(GCD)是異步執(zhí)行的任務(wù)之一顶瞒。
3.1.2 多線程編程
在Objective-C的if語句和for語句等控制語句或函數(shù)調(diào)用的情況下抒痒,執(zhí)行命令列的地址會遠離當(dāng)前的位置(位置偏移)友鼻。但是芭梯,由于一個CPU一次只能執(zhí)行一個命令列,不執(zhí)行某處分開的并列的兩個命令露筒,因此通過CPU執(zhí)行的CPU命令列就好比一條無分叉的大道旬牲,其整不會出現(xiàn)分歧猎贴。
這里所說的“一個CPU執(zhí)行的CPU命令列為一條無分叉路徑”即為線程。這種無分叉路徑不止一條各聘,存在多條即為“多線程揣非。”在多線程中伦吠,一個CPU核執(zhí)行多條不同路徑上的不同命令妆兑。
基本上一個CPU核一次能夠執(zhí)行的CPU命令始終為1.
多線程的問題:數(shù)據(jù)競爭、死鎖毛仪、太多線程導(dǎo)致消耗大量內(nèi)存等搁嗓。
多線程可以保證應(yīng)用程序的響應(yīng)性能。應(yīng)用程序啟動時箱靴,通過最先執(zhí)行的線程腺逛,即“主線程”來描繪用戶界面、處理觸摸屏幕的事件等衡怀。如果該主線程中進行長時間的處理棍矛,如AR用畫像的識別或數(shù)據(jù)庫訪問安疗,就會妨礙主線程德執(zhí)行(阻塞)。在OSX和iOS的應(yīng)用程序中够委,會妨礙主線程中被稱為RunLoop的主循環(huán)的執(zhí)行荐类,從而導(dǎo)致不能更新用戶界面,應(yīng)用程序的書面長時間停滯等問題茁帽。
使用多線程玉罐,在執(zhí)行長時間的處理時仍可保證用戶界面的響應(yīng)性能壤巷。
3.2 GCD的API
3.2.1 Dispatch Queue
GCD官方說明:開發(fā)者要做的只是定義想執(zhí)行的任務(wù)并追加到適當(dāng)?shù)?Dispatch Queue 中监嗜。
dispatch_async(queue, ^{
// 想要執(zhí)行的任務(wù)
});
將Block通過dispatch_async
函數(shù)"追加"賦值在變量queue的“Dispatch Queue中”。Dispatch Queue按照追加的順序(先進先出FIFO)執(zhí)行處理
在執(zhí)行處理時存在兩種Dispatch Queue激率,一種是等待現(xiàn)在執(zhí)行中處理的Serial Disaptch Queue铁追,另一種是不等待現(xiàn)在執(zhí)行中處理的Concurrent Dispatch Queue季蚂。
Dispatch Queue 的種類 | 說明 |
---|---|
Serial Dispatch Queue | 等待現(xiàn)在執(zhí)行中處理結(jié)束 |
Concurent Dispatch Queue | 不等待現(xiàn)在執(zhí)行中處理結(jié)束 |
并行執(zhí)行的處理數(shù)量取決于當(dāng)前系統(tǒng)的狀態(tài)。即 iOS 和 OSX 基于 Dispatch Queue 中的處理數(shù)琅束、CPU核數(shù)以及CPU負荷等當(dāng)前系統(tǒng)的狀態(tài)來決定 Concurent Dispatch Queue 中并行執(zhí)行的處理數(shù)扭屁。所謂“并行執(zhí)行”,就是使用多個線程執(zhí)行多個處理狰闪。
iOS 和 OSX 的核心————XNU內(nèi)核決定應(yīng)當(dāng)使用的線程數(shù),并只生成所需的線程執(zhí)行處理埋泵。另外幔欧,當(dāng)處理結(jié)束,應(yīng)當(dāng)執(zhí)行的處理數(shù)減少時丽声,XNU內(nèi)核會結(jié)束不再需要的內(nèi)核礁蔗。XNU內(nèi)核僅使用 Concurent Dispatch Queue 便可完美地管理并行執(zhí)行多個處理的線程。
3.2.2 dispatch_queue_create
通過dispatch_queue_create
函數(shù)可生成 Dispatch Queue雁社。以下源代碼生成了 Serial Dispatch Queue浴井。
dispatch_queue_t serialQueue =
dispatch_queue_create("com.example.gcddemo.serialqueue", NULL);
在說明dispatch_queue_create
函數(shù)之前,先講一下關(guān)于 Serial Dispatch Queue 生成個數(shù)的注意事項霉撵。
如前所述磺浙, Concurrent Dispatch Queue 并行執(zhí)行多個追加處理,而 Serial Dispatch Queue 同時只能追加一個處理徒坡。雖然 Serial Dispatch Queue 和 Concurrent Dispatch Queue 受到系統(tǒng)資源的限制撕氧, 但用dispatch_queue_create
函數(shù)可生成任意多個 Dispatch Queue。
當(dāng)生成多個 Serial Dispatch Queue 時喇完,各個 Serial Dispatch Queue將并行執(zhí)行伦泥。雖然在一個 Serial Dispatch Queue 中同時只能執(zhí)行一個追加處理,但如果將處理分別追加到4個 Serial Dispatch Queue 中, 各個 Serial Dispatch Queue 執(zhí)行一個不脯,即為同時執(zhí)行四個處理(串行異步)府怯。
如果過多使用多線程,就會消耗大量內(nèi)存防楷,引發(fā)大量的上下文切換牺丙,大幅度降低系統(tǒng)的響應(yīng)性能。
只在為了避免多線程編程問題之一————多個線程更新想用資源所導(dǎo)致數(shù)據(jù)競爭時使用 Serial Dispatch Queue复局。
繼續(xù)說明dispatch_queue_create
函數(shù)赘被,第一個參數(shù)為生成的 queue 的名稱,推薦使用應(yīng)用程序ID這種逆序全程域名(FQDN肖揣, fully qualified domain name),便于在調(diào)試和日志中定位問題浮入。第二個參數(shù)龙优,生成 Serial Dispatch Queue 時可為 DISPATCH_QUEUE_SERIAL
或 NULL, 生成 Concurent Dispatch Queue 時為DISPATCH_QUEUE_CONCURRENT
。
iOS6.0后事秀,Dispatch Queue不再需要程序員負責(zé)釋放(dispatch/_release)彤断。
3.2.3 Main Dispatch Queue / Global Dispatch Queue
Main Dispatch Queue 是在主線程中執(zhí)行的 Dispatch Queue。因為主線程只有一個易迹,所以 Main Dispatch Queue 是 Serial Dispatch Queue宰衙。
追加到 Main Dispatch Queue 的處理在主線程的 RunLoop 中執(zhí)行。由于在主線程中執(zhí)行睹欲,因此要將用戶界面的界面更新等一些必須在主線程中執(zhí)行的處理追加到 Main Dispatch Queue 使用供炼。這正好與NSObject類的 performSelectorOnMainThread
實例方法這一執(zhí)行的方法相同。
Global Dispatch Queue 是所有應(yīng)用程序都能夠使用的 Concurrent Dispatch Queue窘疮。需要時獲取使用即可
Global Dispatch Queue 有4個優(yōu)先級袋哼,但是通過 XNU 內(nèi)核用于 Global Dispatch Queue 的線程并不能保證實時性,因此執(zhí)行優(yōu)先級只是大致的判斷闸衫。
Dispatch Queue的種類
名稱 | Dispatch Queue 的種類 | 說明 |
---|---|---|
Main Dispatch Queue | Serial Dispatch Queue | 主線程執(zhí)行 |
Global Dispatch Queue (High Priority) | Concurrent Dispatch Queue | 執(zhí)行優(yōu)先級:高(最高優(yōu)先) |
Global Dispatch Queue (Default Priority) | Concurrent Dispatch Queue | 執(zhí)行優(yōu)先級:默認 |
Global Dispatch Queue (Low Priority) | Concurrent Dispatch Queue | 執(zhí)行優(yōu)先級:低 |
Global Dispatch Queue (Background Priority) | Concurrent Dispatch Queue | 執(zhí)行優(yōu)先級:后臺 |
3.2.4 dispatch_set_target_queue
dispatch_set_target_queue
是用來變更生成的 Dispatch Queue 的執(zhí)行優(yōu)先級的涛贯。
dispatch_set_target_queue(<#dispatch_object_t _Nonnull object#>, <#dispatch_queue_t _Nullable queue#>)
指定要變更執(zhí)行優(yōu)先級的 Dispatch Queue 為dispatch_set_target_queue
函數(shù)的第一個參數(shù),指定與要使用的執(zhí)行優(yōu)先級相同的 Dispatch Queue 為第二個參數(shù)(目標)蔚出。
將 Dispatch Queue 指定為 dispatch_set_target_queue
函數(shù)的參數(shù)弟翘,不僅可以變更 Dispatch Queue 的執(zhí)行優(yōu)先級,還可以作成 Dispatch Queue 的執(zhí)行階層骄酗。如果多個 Serial Dispatch Queue 中使用dispatch_set_target_queue
函數(shù)指定目標為某一個 Serial Dispatch Queue 稀余,那么原本應(yīng)并行執(zhí)行的多個 Serial Dispatch Queue ,在目標 Serial Dispatch Queue 上只能同時執(zhí)行一個處理酥筝。
在必須將不可并行執(zhí)行的處理追加到多個 Serial Dispatch Queue 中時滚躯,如果使用dispatch_set_target_queue
函數(shù)將目標指定為某一個 Serial Dispatch Queue,即可防止處理并行執(zhí)行。
3.2.5 dispatch_after
dispatch_after
函數(shù)并不是在指定時間后執(zhí)行處理掸掏,而只是在指定時間追加處理到 Dispatch Queue茁影。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"waited after at least three seconds");
});
因為 Main Dispatch Queue 在主線程的 RunLoop 執(zhí)行,所以在比如每隔1/60秒執(zhí)行的 RunLoop 中丧凤,Block 最快在3秒后募闲,最慢在3秒+1/60秒后執(zhí)行,并且在 Main Dispatch Queue 有大量處理追加或主線程處理本身有延遲時愿待,這個時間會更長浩螺。
第一個參數(shù)是指定時間用的dispatch_time_t
類型的值。該值使用dispatch_time
函數(shù)(計算相對時間)或dispatch_walltime
函數(shù)(計算絕對時間)仍侥。
3.2.6 Dispatch Group
當(dāng)追加到的 Dispatch Queue 中的多個處理全部結(jié)束后想執(zhí)行結(jié)束處理要出,使用 Dispatch Group。
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(@"blk0");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk2");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});
執(zhí)行結(jié)果
blk0
blk1
blk2
done
在 Dispatch Group 中也可以使用 dispatch_group_wait
函數(shù)僅等待全部處理執(zhí)行結(jié)束农渊。
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í)行結(jié)束
} else {
// 屬于 Dispatch Group 的某一個處理還在執(zhí)行
}
當(dāng)dispatch_group_wait
函數(shù)返回的等待時間為DISPATCH_TIME_FOREVER
時患蹂,意味著永久等待,屬于 Dispatch Group 的處理必定全部執(zhí)行結(jié)束砸紊,因此返回值恒為0传于。
需要注意的是,一旦調(diào)用dispatch_group_wait
函數(shù)醉顽,該函數(shù)就處于調(diào)用的狀態(tài)而不返回沼溜。即執(zhí)行dispatch_group_wait
函數(shù)的現(xiàn)在的線程(當(dāng)前線程)停止。在經(jīng)過dispatch_group_wait
函數(shù)中指定的時間或?qū)儆谥付?Dispatch Group 的處理全部執(zhí)行結(jié)束之前游添,執(zhí)行該函數(shù)的線程停止系草。
指定DISPATCH_TIME_NOW
,則不用任何等待即可判定屬于 Dispatch Group 的處理是否執(zhí)行結(jié)束否淤。
3.2.7 dispatch_barrier_async
dispatch_queue_t queue = dispatch_queue_create("com.example.gcddemo.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(dataQueue, ^{
NSLog(@"read data 1");
});
dispatch_async(dataQueue, ^{
NSLog(@"read data 2");
});
dispatch_barrier_async(dataQueue, ^{
NSLog(@"write data 1");
});
dispatch_async(dataQueue, ^{
NSLog(@"read data 3");
});
dispatch_barrier_async
函數(shù)會等待之前追加到 Dispatch Queue 中的處理全部執(zhí)行結(jié)束后悄但,再將dispatch_barrier_async
函數(shù)指定的處理追加到 Dispatch Queue 中,然后在dispatch_barrier_async
函數(shù)追加的處理執(zhí)行完畢后石抡,繼續(xù)執(zhí)行dispatch_barrier_async
函數(shù)之后追加到 Dispatch Queue 的處理檐嚣。
使用 Concurrent Dispatch Queue 和 dispatch_barrier_async
函數(shù)可實現(xiàn)搞笑的數(shù)據(jù)庫訪問和文件訪問。
3.2.8 dispatch_sync
dispatch_async
(異步)啰扛,指定的 Block ”非同步“地追加到指定的 Dispatch Queue 中嚎京,dispatch_async
函數(shù)不做任何等待。
dispatch_sync
(同步)隐解,將指定的Block”同步“追加到指定的 Dispatch Queue 中鞍帝,在追加Block結(jié)束之前,dispatch_sync
函數(shù)會一直等待煞茫。
使用dispatch_sync
函數(shù)需要注意死鎖問題帕涌。
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"123");
});
該源碼在主線程中執(zhí)行指定的 Block摄凡,并等待其執(zhí)行結(jié)束,而其實在主線程中正在執(zhí)行這些源代碼蚓曼,所以無法追加到主線程的 Block亲澡。
3.2.9 dispatch_apply
da函數(shù)是ds函數(shù)和 dg 的關(guān)聯(lián)API。該函數(shù)按指定的次數(shù)將指定的Block追加到指定的 Dispatch Queue 中纫版,并等待全部處理執(zhí)行結(jié)束床绪。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu", index);
});
NSLog(@"done");
執(zhí)行結(jié)果
0
1
3
2
5
4
6
7
8
9
done
因為在 Global Dispatch Queue 中執(zhí)行處理,所以各個處理的執(zhí)行時間不定其弊。但是輸出結(jié)果最后的done必定在最后的位置上癞己。這是因為da函數(shù)會等待全部處理執(zhí)行結(jié)束。
由于da函數(shù)與ds函數(shù)相同梭伐,會等待處理執(zhí)行結(jié)束痹雅,因此推薦在dispatch_async
函數(shù)中非同步地執(zhí)行da函數(shù)。
對比for循環(huán)中使用dispatch_async
函數(shù)糊识,并行的機制比串行的機制更安全练慕、快速。
3.2.10 dispatch_suspend/dispatch_resume
當(dāng)追加大量處理到 Dispatch Queue 時,在追加處理的過程中,有時希望不執(zhí)行已追加的處理技掏。這種情況可以使用dispatch_suspend
函數(shù)掛起指定的 Dispatch Queue,當(dāng)可以執(zhí)行時再使用dispatch_resume
函數(shù)恢復(fù)指定的 Dispatch Queue项鬼。
dispatch_suspend
函數(shù)不會立即停止正在執(zhí)行的 Block哑梳,而是在當(dāng)前 Block 執(zhí)行完成后,暫停后續(xù)的 Block 執(zhí)行绘盟。
3.2.11 Dispatch Semaphore
當(dāng)并行執(zhí)行的處理更新數(shù)據(jù)時鸠真,會產(chǎn)生數(shù)據(jù)不一致的情況,有時應(yīng)用程序還會異常結(jié)束龄毡。雖然使用 Serial Dispatch Queue 和dispatch_barrier_async
函數(shù)可避免這類問題吠卷,但有必要進行更細粒度的排他控制,這時可以使用 Dispatch Semaphore沦零。
Dispatch Semaphore 是持有計數(shù)的信號祭隔,該計數(shù)是多線程編程中的計數(shù)類型信號。所謂信號路操,類似于過馬路時常用的手旗疾渴,可以通過時舉起手旗,不可通過時放下手旗屯仗。而在 Dispatch Semaphore 中搞坝,使用計數(shù)來實現(xiàn)該功能。計數(shù)為0時等待魁袜,計數(shù)為1或大于1時桩撮,減去1而不等待敦第。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
生成 Dispatch Semaphore
Dispatch Semaphore的計數(shù)初始值設(shè)定為“1”
保證可訪問NSMutableArray類對象的線程同時只有一個
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
/*
等待 Dispatch Semaphore
一直等待,直到 Dispatch Semaphore 的計數(shù)值達到大于等于1
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
由于 Dispatch Semaphore 的計數(shù)值達到大于等于1
所以將 Dispatch Semaphore 的計數(shù)值減去1
dispatch_semaphore_wait函數(shù)執(zhí)行返回店量。
即執(zhí)行到此時的 Dispatch Semaphore 的計數(shù)值恒為0
由于可訪問NSMutableArray類對象的線程只有1個
因此可安全地進行更新
*/
[array addObject:[NSNumber numberWithInt:i]];
/*
排他控制處理結(jié)束
所以通過dispatch_semaphore_signal函數(shù)將
Dispatch Semaphore 的計數(shù)值加1芜果。
如果有通過dispatch_semaphore_wait函數(shù)
等待 Dispatch Semaphore 的計數(shù)值增加的線程
就由最先等待的線程執(zhí)行。
*/
dispatch_semaphore_signal(semaphore);
});
}
NSLog(@"done");
3.2.12 dispatch_once
dispatch_once
函數(shù)式保證在應(yīng)用程序執(zhí)行中只執(zhí)行一次指定處理的API垫桂。多線程環(huán)境下也可保證百分百安全师幕。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// code to be executed once
});
3.2.13 Dispatch I/O
在讀取較大文件時,如果將文件分成合適的大小并使用 Global Dispatch Queue 并列讀取的話能提高效率∥芴玻現(xiàn)今的輸入/輸出硬件已經(jīng)可以做到一次使用多個線程更快地并列讀取了霹粥。能實現(xiàn)這一功能的就是 Dispatch I/O 和 Dispatch Data。
通過Dispatch I/O讀寫文件時疼鸟,使用 Global Dispatch Queue 將1個文件按某個大小 read/write后控。
dispatch_sync(queue, ^{/* 讀取 0 ~ 8191 字節(jié)*/});
dispatch_sync(queue, ^{/* 讀取 8192 ~ 16383 字節(jié)*/});
dispatch_sync(queue, ^{/* 讀取 16384 ~ 24575 字節(jié)*/});
dispatch_sync(queue, ^{/* 讀取 24576 ~ 32767 字節(jié)*/});
像上面這樣,將文件分割為一塊一塊地進行讀取處理空镜。
以下為蘋果中使用 Dispatch I/O 和 Dispatch Data 的示例浩淘。
pipe_q = dispatch_queue_create("PipeQ",NULL);
pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM,fd,pipe_q,^(int err){
close(fd);
});
*out_fd = fdpair[i];
dispatch_io_set_low_water(pipe_channel,SIZE_MAX);
dispatch_io_read(pipe_channel,0,SIZE_MAX,pipe_q, ^(bool done,dispatch_data_t pipe data,int err){
if(err == 0)
{
size_t len = dispatch_data_get_size(pipe data);
if(len > 0)
{
const char *bytes = NULL;
char *encoded;
dispatch_data_t md = dispatch_data_create_map(pipe data,(const void **)&bytes,&len);
asl_set((aslmsg)merged_msg,ASL_KEY_AUX_DATA,encoded);
free(encoded);
_asl_send_message(NULL,merged_msg,-1,NULL);
asl_msg_release(merged_msg);
dispatch_release(md);
}
}
if(done)
{
dispatch_semaphore_signal(sem);
dispatch_release(pipe_channel);
dispatch_release(pipe_q);
}
});
dispatch_io_create
函數(shù)生成 Dispatch I/O,并指定發(fā)生錯誤時用來執(zhí)行處理的 Block吴攒,以及執(zhí)行該 Block的 Dispatch Queue张抄。dispatch_io_set_low_water
函數(shù)設(shè)定一次讀取的大小(分割大型菡)署惯,dispatch_io_read
函數(shù)使用 Global Dispatch Queue 開始并列讀取。每當(dāng)各個分割的文件快讀取結(jié)束時镣隶,將含有文件塊數(shù)據(jù)的 Dispatch Data 傳遞給dispatch_io_read
函數(shù)指定的讀取結(jié)束時回調(diào)用的 Block极谊。回調(diào)用的 Block 分析傳遞過來的 Dispatch Data 并進行結(jié)合處理安岂。
如果想提高文件讀取速度轻猖,可以嘗試使用Dispatch I/O。