1.1 什么是GCD
Grand Central Dispatch是異步執(zhí)行任務(wù)的技術(shù)之一停局。一般將應(yīng)用程序中記述的線程管理用的代碼在系統(tǒng)級(jí)中實(shí)現(xiàn)阵翎。開發(fā)者只需要定義想要執(zhí)行的任務(wù)并追加到適當(dāng)?shù)腄ispatch Queue中,GCD就能生成必要的線程并計(jì)劃執(zhí)行任務(wù)。由于線程管理是作為系統(tǒng)的一部分來實(shí)現(xiàn)的。隱藏可以統(tǒng)一管理模蜡。也可以執(zhí)行任務(wù)。這樣就比一切的線程更有效率扁凛。
1.2 多線程編程
線程
- 一個(gè)CPU執(zhí)行的CPU命令列為一條無分叉路徑忍疾,即為“線程”
定理
- 一個(gè)CPU核執(zhí)行的CPU命令列為一條無分叉路徑
-
這種無分叉路徑不止一條,存在有多條時(shí)即為“多線程”谨朝。在多線程中卤妒,一個(gè)CPU核執(zhí)行多條不同路徑上的不同命令。
Snip20171221_17.png
上下文切換
CPU的寄存器等信息保存到各自路徑專用的內(nèi)存塊中字币,從切換目標(biāo)路徑專用的內(nèi)存塊中则披,復(fù)原CPU寄存器等信息,繼續(xù)執(zhí)行切換路徑的CPU命令列洗出。這種被稱為“上下文切換”士复。
多線程編程
由于使用多線程的程序可以在某個(gè)線程和其他線程之間反復(fù)多次進(jìn)行上下文切換,因此看上去就好像1個(gè)CPU核能夠并列地執(zhí)行多個(gè)線程一樣翩活。而且在具有多個(gè)CPU核的情況下阱洪,就不是“看上去像了“便贵,而是真的提供了多個(gè)CPU核并行執(zhí)行多個(gè)線程的技術(shù)。
多線程編程實(shí)際上是一種易發(fā)生各種問題的編程技術(shù)冗荸。比如多個(gè)線程更新相同的資源會(huì)導(dǎo)致數(shù)據(jù)的不一致(數(shù)據(jù)競(jìng)爭)嫉沽,停止等待事件的線程會(huì)導(dǎo)致多個(gè)線程相互持續(xù)等待(死鎖),使用太多線程會(huì)消耗大量內(nèi)存等俏竞。
多線程的優(yōu)點(diǎn)
弊端:應(yīng)用程序在啟動(dòng)時(shí),通過最先執(zhí)行的線程堂竟,即”主線程“來描述用戶界面魂毁,處理觸摸屏幕的事件等。如果在該主線程中進(jìn)行長時(shí)間的處理出嘹,如AR用畫像的識(shí)別貨數(shù)據(jù)庫訪問席楚,就會(huì)妨礙主線程的執(zhí)行(阻塞)。在OS X和iOS的應(yīng)用程序中税稼,會(huì)妨礙主線程中被稱為RunLoop的主循環(huán)執(zhí)行烦秩,從而導(dǎo)致不能更新用戶界面,應(yīng)用程序的畫面長時(shí)間停滯等問題郎仆。
這就是長時(shí)間的處理不在主線程中執(zhí)行而在其他線程中執(zhí)行的原因只祠。
使用多線程編程,在執(zhí)行長時(shí)間的處理時(shí)間時(shí)仍可以保證用戶界面的響應(yīng)性能扰肌。
2. GCD的API
GCD的說明:開發(fā)者要做的只是定義想執(zhí)行的任務(wù)并追加到適當(dāng)?shù)腄ispatch Queue中抛寝。
2.1 Dispatch Queue
是執(zhí)行處理的等待隊(duì)列。應(yīng)用程序編程人員通過dispatch_async函數(shù)等API曙旭,在Block語法中記述想執(zhí)行的處理并將其追加到Dispatch Queue中盗舰。Dispatch Queue按照追加的順序(FIFO)執(zhí)行處理。
在執(zhí)行處理時(shí)存在兩種Dispatch Queue桂躏,一種是等待現(xiàn)在執(zhí)行中處理的Serial Dispatch Queue钻趋,另一種是不等待現(xiàn)在執(zhí)行中處理的Concurrent Dispatch Queue。
并行執(zhí)行
就是使用多個(gè)線程同時(shí)執(zhí)行多個(gè)處理
2.2 dispatch_queue_create
通過dispatch_queue_create函數(shù)可生成Dispatch Queue剂习。
-
多個(gè)Serial Dispatch Queue 可并行執(zhí)行
Snip20171222_24.png
如果過多使用多線程蛮位,就會(huì)消耗大量內(nèi)存,引起大量的上下文切換鳞绕,大幅度降低系統(tǒng)的響應(yīng)性能土至。
- 當(dāng)想并行執(zhí)行不發(fā)生數(shù)據(jù)競(jìng)爭等問題的處理時(shí),使用Concurrent Dispatch Queue猾昆。而且對(duì)于Concurrent Dispatch Queue來說陶因,不管生成多少。由于XNU內(nèi)核只使用有效管理的線程垂蜗,因此不會(huì)發(fā)生Serial Dispatch Queue的那些問題楷扬。
2.3 Main Dispatch Queue / Global Dispatch Queue
第二種方法是獲取系統(tǒng)標(biāo)準(zhǔn)提供的Dispatch Queue解幽。
- Main Dispatch Queue
- 是在主線程中執(zhí)行的Dispatch Queue,因?yàn)橹骶€程只有一個(gè)烘苹,所以Main Dispatch Queue自然就是Serial Dispatch Queue串行隊(duì)列躲株。
-
追加到Main Dispatch Queue的處理在主線程的RunLoop中執(zhí)行。由于在主線程中執(zhí)行镣衡,因此要將用戶界面的界面更新等一些必須在主線程中執(zhí)行的處理追加到Main Dispatch使用霜定。
Snip20171222_25.png
- Global Dispatch Queue
- 另一個(gè)Global Dispatch Queue是所有應(yīng)用程序都能夠使用的Concurrent Dispatch Queue。沒有必要通過dispatch_queue_create函數(shù)逐個(gè)生成Concurrent Dispatch Queue廊鸥。只要獲取Global Dispatch Queue使用即可望浩。
- Global Dispatch Queue有四個(gè)優(yōu)先級(jí),分別是高優(yōu)先級(jí)(High Priority)惰说,默認(rèn)優(yōu)先級(jí)(Default Priority)磨德,低優(yōu)先級(jí)(Low Priority)和后臺(tái)優(yōu)先級(jí)(Background Priority)。
對(duì)于Main Dispatch Queue和Global Dispatch Queue執(zhí)行dispatch_retain函數(shù)和dispatch_release函數(shù)不會(huì)引起任何變化吆视,也不會(huì)有任何問題典挑。這也是獲取并使用Global Dispatch Queue比生成,使用啦吧,釋放Concurrent Dispatch Queue更輕松的原因您觉。
2.4 dispatch_set_target_queue
dispatch_queue_create函數(shù)生成的Dispatch Queue不管是Serial Dispatch Queue還是Concurrnet 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ù)顾犹。
在必須將不可并行執(zhí)行的處理追加到多個(gè)Serial Dispatch Queue中時(shí),如果使用dispatch_set_target_queue函數(shù)將目標(biāo)指定為某一個(gè)Serial Dispatch Queue褒墨,即可防止處理并行執(zhí)行炫刷。
2.5 dispatch_after
- dispatch_after函數(shù)并不是在指定時(shí)間后執(zhí)行處理,而只是在指定時(shí)間追加處理到Dispatch Queue郁妈,此源代碼與在3秒后用dispatch_async函數(shù)追加Block到Main Dispatch Queue的相同浑玛。
- 因?yàn)镸ain Dispatch Queue在主線程的Runloop中執(zhí)行,所以在此比如每隔1/60秒執(zhí)行的Runloop中噩咪,Block最快在3秒后執(zhí)行顾彰,最慢在3秒+1/60秒后執(zhí)行,并且在Main Dispatch Queue有大量處理追加或主線程的處理本身有延時(shí)時(shí)胃碾,這個(gè)時(shí)間會(huì)更長涨享。
- dispatch_time函數(shù)通常用于計(jì)算相對(duì)時(shí)間,而dispatch_walltime函數(shù)用于計(jì)算絕對(duì)時(shí)間仆百。
2.6 Dispatch Group
在追加到Dispatch Queue中的多個(gè)處理全部結(jié)束后想執(zhí)行結(jié)束處理的需求厕隧。
- 向Global Dispatch Queue即Concurrent Dispatch Queue追加處理,多個(gè)線程并行執(zhí)行,所以追加處理的執(zhí)行順序不定吁讨。執(zhí)行時(shí)會(huì)發(fā)生變化髓迎,但是此執(zhí)行結(jié)束的done一定是最后輸出的。
- 無論向什么樣的Dispatch Queue中追加處理建丧,使用Dispatch Group都可以監(jiān)聽這些處理執(zhí)行的結(jié)束排龄。一旦檢測(cè)到所有處理執(zhí)行結(jié)束,就可以將結(jié)束的處理追加到Dispatch Queue中翎朱,這就是使用Dispatch Group的原因橄维。
- 在Dispatch Group中也可以使用dispatch_group_wait函數(shù)僅等待全部處理執(zhí)行結(jié)束。
2.7 dispatch_barrier_async
使用dispatch_barrier_async函數(shù)拴曲,dispatch_barrier_async函數(shù)會(huì)等待追加到Concurrent Dispatch Queue上的并行執(zhí)行的處理全部結(jié)束后争舞,再將指定的處理追加到改Concurrent Dispatch Queue中,然后再有dispatch_barrier_async函數(shù)追加的處理執(zhí)行完畢之后疗韵,Concurrent Dispatch Queue才恢復(fù)為一般的動(dòng)作,追加到該Concurrent Dispatch Queue的處理又開始并行執(zhí)行侄非。
使用Concurrent Dispatch Queue和dispatch_barrier_async函數(shù)可實(shí)現(xiàn)高效的數(shù)據(jù)庫訪問和文件訪問蕉汪。
2.8 dispatch_sync
dispatch_async函數(shù)的“async”意味著“非同步”(asynchronous),就是將指定的Block“非同步”地追加到指定的Dispatch Queue中逞怨。dispatch_async函數(shù)不做任何等待者疤。
dispatch_sync同步,(synchronous),即將指定的Block“同步”追加到指定的Dispatch Queue中叠赦。在追加Block結(jié)束之前驹马,dispatch_sync函數(shù)會(huì)一直等待。
等待意味著當(dāng)前線程停止
一旦調(diào)用dispatch_sync函數(shù)除秀,那么在指定的處理執(zhí)行結(jié)束之前糯累,該函數(shù)不會(huì)返回。dispatch_sync函數(shù)可以簡化源代碼册踩,也可說是簡易版的dispatch_group_wait函數(shù)泳姐。
正因?yàn)閐ispatch_sync函數(shù)使用簡單,所以容易引起問題暂吉,即死鎖胖秒。
例如如果在主線程中執(zhí)行以下源代碼就會(huì)死鎖
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"Hello?");
});
改源代碼在Main Dispatch Queue即主線程中執(zhí)行指定的Block,并等待其執(zhí)行結(jié)束慕的。而其實(shí)在主線程中正在執(zhí)行這些源代碼阎肝,所以無法執(zhí)行追加到Main Dispatch Queue的Block。
下面的例子也一樣
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
dispatch_sync(queue,^{
NSLog(@"Hello?");
});
});
Mian Dispatch Queue中執(zhí)行的Block等待Main Dispatch Queue中要執(zhí)行的Block執(zhí)行結(jié)束肮街。
當(dāng)然Serial Dispatch Queue 也會(huì)引起相同的問題风题。
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue",NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"Hello?");
});
});
另外,由Dispatch_barrier_async函數(shù)中含有async可推出出,相應(yīng)的也有dispatch_barrier_sync函數(shù)俯邓。dispatch_barrier_async函數(shù)的作用是在等待追加的處理全部執(zhí)行結(jié)束后骡楼,再追加處理到Dispatch Queue中,此外稽鞭,它還與dispatch_sync函數(shù)相同鸟整,會(huì)等待追加處理的執(zhí)行結(jié)束。
2.9 dispatch_apple
dispatch_apple函數(shù)是Dispatch_sync函數(shù)和Dispatch Group的關(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");
結(jié)果如下:
1
2
3
0
4
5
6
7
8
9
done
因?yàn)樵贕lobal Dispatch Queue中執(zhí)行處理吩抓,所以各個(gè)處理的時(shí)間不定涉茧。但是輸出結(jié)果中最后的done必定在最后的位置上。這是因?yàn)閐ispatch_apple函數(shù)會(huì)等待全部處理執(zhí)行結(jié)束疹娶。
NSArray *array = @[@0,@1,@2,@3,@4];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%zu:%@",index,[array objectAtIndex:index]);
});
NSLog(@"done");
結(jié)果:
3:3
0:0
1:1
2:2
4:4
done
這樣可以簡單地在Global Dispatch Queue中對(duì)所有的元素執(zhí)行Block
另外伴栓,由于dispatch_apply函數(shù)也與dispatch_sync函數(shù)相同,會(huì)等待處理執(zhí)行結(jié)束雨饺,因此推薦在dispatch_async函數(shù)非同步地執(zhí)行dispatch_apply函數(shù)钳垮。
NSArray *array = @[@0,@1,@2,@3,@4];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 在Global Dispatch Queue中非同步執(zhí)行
dispatch_async(queue, ^{
// 等待dispatch_apply函數(shù)中全部處理執(zhí)行結(jié)束
dispatch_apply([array count], queue, ^(size_t index) {
// 并列處理包含在NSArray對(duì)象的全部對(duì)象
NSLog(@"%zu:%@",index,[array objectAtIndex:index]);
});
// dispatch_apply函數(shù)中的處理全部執(zhí)行結(jié)束
// 在Main Dispatch Queue中非同步執(zhí)行
dispatch_async(dispatch_get_main_queue(), ^{
// 在Main Dispatch Queue中執(zhí)行處理用戶界面更新等等
NSLog(@"done 界面刷新");
});
});
執(zhí)行結(jié)果:
1:1
2:2
3:3
0:0
4:4
done 界面刷新
2.10 dispatch_suspend/dispatch_resume
當(dāng)追加大量處理到Dispatch Queue時(shí),在追加處理的過程中额港,有時(shí)希望不執(zhí)行已追加的處理饺窿。在這種情況下,只要掛起Dispatch Queue即可移斩。當(dāng)可以執(zhí)行時(shí)再恢復(fù)肚医。
- dispatch_suspend函數(shù)掛起指定的Dispatch Queue
dispatch_suspend(queue);
- Dispatch_resume函數(shù)恢復(fù)指定的Dispatch Queue
dispatch_resume(queue);
這些函數(shù)對(duì)已經(jīng)執(zhí)行的處理沒有影響。掛起后向瓷,追加到Dispatch Queue中但尚未執(zhí)行的處理在此之后停止執(zhí)行肠套。而恢復(fù)則使這些處理能夠繼續(xù)執(zhí)行。
2.11 Dispatch Semaphonre
如前所述猖任,當(dāng)并行執(zhí)行的處理更新數(shù)據(jù)時(shí)糠排,會(huì)產(chǎn)生數(shù)據(jù)不一致的情況,有時(shí)應(yīng)用程序還會(huì)導(dǎo)致異常結(jié)束超升。雖然使用Serial Dispatch Queue和disaptch_barrier_async函數(shù)可以避免這類問題入宦,但有必要進(jìn)行更細(xì)粒度的為他控制。
以下場(chǎng)景室琢,不考慮順序乾闰,將所有數(shù)據(jù)追加到NSMutableArray中。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];;
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
因?yàn)樵撛创a使用Global Dispatch更新NSMutableArray類對(duì)象盈滴,所以執(zhí)行后由內(nèi)存錯(cuò)誤導(dǎo)致應(yīng)用程序異常結(jié)束的概率很高涯肩。此時(shí)應(yīng)使用Dispatch Semaphore轿钠。
- Disaptch Semaphore
Dispatch Semaphore是持有計(jì)數(shù)的信號(hào),該計(jì)數(shù)是多線程編程中的計(jì)數(shù)類型信號(hào)病苗。所謂信號(hào)疗垛,類似于過馬路時(shí)常用的手旗×螂可以通過時(shí)舉起手旗贷腕,不可通過時(shí)放下手旗。而在Dispatch Semaphore中咬展,使用計(jì)數(shù)來實(shí)現(xiàn)該功能泽裳。計(jì)數(shù)為零時(shí)等待,計(jì)數(shù)為1貨大于1時(shí)破婆,減去1而不等待涮总。
dispatch_semaphore_wait函數(shù)返回0時(shí),可安全地執(zhí)行需要進(jìn)行排他控制的處理祷舀。改處理結(jié)束時(shí)通過dispatch_semaphore_signal函數(shù)將Dispatch Semaphore額計(jì)數(shù)值加1.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 生成dispatch Semaphore瀑梗,并且計(jì)數(shù)初始值設(shè)定為1.保證可訪問NSMutableArray類對(duì)象的線程同時(shí)只能有1個(gè)
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
// 等待Dispatch Semaphore,一直等待,直到Dispatch Semaphore的計(jì)數(shù)值達(dá)到大于等于1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[array addObject:[NSNumber numberWithInt:i]];
// 排他控制處理結(jié)束裳扯,通過dispatch_semaphore_signal函數(shù)將Dispatch Semaphore的計(jì)數(shù)值加1
dispatch_semaphore_signal(semaphore);
});
}
在沒有Serial Dispatch和dispatch_barrier_async函數(shù)那么大粒度并且一部分處理需要進(jìn)行排他控制的情況下抛丽,Dispatch Semaphore便可發(fā)揮威力.
2.12 dispatch_once
dispatch_once函數(shù)是保證在應(yīng)用程序中只執(zhí)行一次指定處理的API。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//初始化操作
});
通過dispatch_once函數(shù)嚎朽,該源代碼即使在多線程環(huán)境下執(zhí)行铺纽,也可以保證百分之百的安全柬帕。
2.13 Dispatch I/0
在讀取較大文件時(shí)哟忍,如果將文件分成合適的大小并使用Global Dispatch Queue并列讀取,會(huì)比一般的讀取速度快很多∠萸蓿現(xiàn)今的輸入/輸出硬件已經(jīng)可以做到一次使用多個(gè)線程更快地并列讀取了锅很。能實(shí)現(xiàn)這一功能的就是Dispatch I/0和Dispatch Data。
如果想提高文件讀取的速度凤跑,可以嘗試使用Dispatch I/O
3. GCD的實(shí)現(xiàn)
3.1 Dispatch Queue
GCD的Dispatch Que非常方便爆安,它是如何實(shí)現(xiàn)的呢?
- 用于管理追加的Block得C語言層實(shí)現(xiàn)的FIFO隊(duì)列
- Atomic函數(shù)中實(shí)現(xiàn)的用于排他控制的輕量級(jí)信號(hào)
- 用于管理現(xiàn)場(chǎng)的C語言層實(shí)現(xiàn)的一些容器
通常仔引,應(yīng)用程序中編寫的線程管理用的代碼要在系統(tǒng)級(jí)實(shí)現(xiàn)
系統(tǒng)級(jí)即iOS和OS X的核心XNU內(nèi)核級(jí)上實(shí)現(xiàn)
3.2 Dispatch Source
GCD中除了注意的Dispatch Queue外扔仓,還有不太引人注目的Dispatch Source。它是BSD系內(nèi)核慣有功能kqueue的包裝咖耘。
Kqueue是在XNU內(nèi)核中發(fā)生各種事件時(shí)翘簇,在應(yīng)用編程方執(zhí)行處理的技術(shù)。其CPU負(fù)荷非常小儿倒,盡量不占用資源版保。kqueue可以說是應(yīng)用程序處理XNU內(nèi)核中發(fā)生的各種事件的方法中最優(yōu)秀的一種。
Dispatch Source與Dispatch Queue不同,是可以取消的彻犁。而且取消時(shí)必須執(zhí)行的處理可指定為調(diào)用的Block形式叫胁。因此使用Dispatch Source實(shí)現(xiàn)XNU內(nèi)核中發(fā)生的事件處理要比直接使用kqueue實(shí)現(xiàn)更為簡單。在必須使用kqueue的情況下希望大家還是使用Dispatch Source汞幢,它比較簡單驼鹅。