原文鏈接:蘋果核
相信讀者已經看過很多大神們對GCD深入淺出的分析,這也是老生常談的一個多線程的實現方式了,所以我也就不再啰嗦其理論。但是到底有多少方法是我們日常編程中常用的?又有多少是你不知道的?今天,我就來例舉一些GCD的方法,絕對讓你看一眼就會正確得使用。
與其說CGD是線程管理,不如說是隊列管理,那么我們先來介紹一下GCD中常用的隊列:
Serial Diapatch Queue 串行隊列
當任務相互依賴,具有明顯的先后順序的時候,使用串行隊列是一個不錯的選擇
創建一個串行隊列:
dispatch_queue_t serialDiapatchQueue=dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_SERIAL);
第一個參數為隊列名,第二個參數為隊列類型,當然,第二個參數人如果寫NULL,創建出來的也是一個串行隊列。然后我們在異步線程來執行這個隊列:
dispatch_async(serialDiapatchQueue, ^{
NSLog(@"1");
});
dispatch_async(serialDiapatchQueue, ^{
sleep(2);
NSLog(@"2");
});
dispatch_async(serialDiapatchQueue, ^{
sleep(1);
NSLog(@"3");
});
為了能更好的理解,我給每個異步線程都添加了一個log,看一下日志平臺的log:
2016-03-07 10:17:13.907 GCD[2195:61497] 1
2016-03-07 10:17:15.911 GCD[2195:61497] 2
2016-03-07 10:17:16.912 GCD[2195:61497] 3
沒錯,他在61497這個編號的線程中做了串行輸出,相互彼此依賴,串行執行
Concurrent Diapatch Queue 并發隊列
與串行隊列剛好相反,他不會存在任務間的相互依賴。
創建一個并發隊列:
dispatch_queue_t concurrentDiapatchQueue=dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
比較2個隊列的創建,我們發現只有第二個參數從DISPATCH_QUEUE_SERIAL
變成了對應的DISPATCH_QUEUE_CONCURRENT
,其他完全一樣。
用同一段代碼,換一種隊列我們來比較一下效果:
dispatch_async(concurrentDiapatchQueue, ^{
NSLog(@"1");
});
dispatch_async(concurrentDiapatchQueue, ^{
sleep(2);
NSLog(@"2");
});
dispatch_async(concurrentDiapatchQueue, ^{
sleep(1);
NSLog(@"3");
});
輸出的log:
2016-03-07 10:42:38.289 GCD[2260:72557] 1
2016-03-07 10:42:39.291 GCD[2260:72559] 3
2016-03-07 10:42:40.293 GCD[2260:72556] 2
我們發現,log的輸出在3個不同編號的線程中進行,而且相互不依賴,不阻塞。
Global Queue & Main Queue
這是系統為我們準備的2個隊列:
- Global Queue其實就是系統創建的Concurrent Diapatch Queue
- Main Queue 其實就是系統創建的位于主線程的Serial Diapatch Queue
通常情況我們會把這2個隊列放在一起使用,也是我們最常用的開異步線程-執行異步任務-回主線程的一種方式:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"異步線程");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"異步主線程");
});
});
通過上面的代碼我們發現了2個有意思的點:
-
dispatch_get_global_queue
存在優先級,沒錯,他一共有4個優先級:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSLog(@"4");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSLog(@"3");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"2");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"1");
});
在指定優先級之后,同一個隊列會按照這個優先級執行,打印的順序為1、2、3、4,當然這不是串行隊列,所以不存在絕對回調先后。
-
異步主線程
- 在日常工作中,除了在其他線程返回主線程的時候需要用這個方法,還有一些時候我們在主線程中直接調用異步主線程,這是利用dispatch_async的特性:block中的任務會放在主線程本次runloop之后返回。這樣,有些存在先后順序的問題就可以得到解決了。
說完了隊列,我們再說說GCD提供的一些操作隊列的方法
dispatch_set_target_queue
剛剛我們說了系統的Global Queue是可以指定優先級的,那我們如何給自己創建的隊列執行優先級呢?這里我們就可以用到dispatch_set_target_queue
這個方法:
dispatch_queue_t serialDiapatchQueue=dispatch_queue_create("com.test.queue", NULL);
dispatch_queue_t dispatchgetglobalqueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_set_target_queue(serialDiapatchQueue, dispatchgetglobalqueue);
dispatch_async(serialDiapatchQueue, ^{
NSLog(@"我優先級低,先讓讓");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"我優先級高,我先block");
});
我把自己創建的隊列塞到了系統提供的global_queue
隊列中,我們可以理解為:我們自己創建的queue其實是位于global_queue
中執行,所以改變global_queue
的優先級,也就改變了我們自己所創建的queue的優先級。所以我們常用這種方式來管理子隊列。
dispatch_after
這個是最常用的,用來延遲執行的GCD方法,因為在主線程中我們不能用sleep來延遲方法的調用,所以用它是最合適的,我們做一個簡單的例子:
NSLog(@"小破孩-波波1");
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSLog(@"小破孩-波波2");
});
輸出的結果:
2016-03-07 11:25:06.019 GCD[2443:95722] 小破孩-波波1
2016-03-07 11:25:08.019 GCD[2443:95722] 小破孩-波波2
我們看到他就是在主線程,就是剛好延遲了2秒,當然,我說這個2秒并不是絕對的,為什么這么說?還記得我之前在介紹dispatch_async
這個特性的時候提到的嗎?他的block中方法的執行會放在主線程runloop之后,所以,如果此時runloop周期較長的時候,可能會有一些時差產生。
dispatch_group
當我們需要監聽一個并發隊列中,所有任務都完成了,就可以用到這個group,因為并發隊列你并不知道哪一個是最后執行的,所以以單獨一個任務是無法監聽到這個點的,如果把這些單任務都放到同一個group,那么,我們就能通過dispatch_group_notify
方法知道什么時候這些任務全部執行完成了。
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(@"0");});
dispatch_group_async(group, queue, ^{NSLog(@"1");});
dispatch_group_async(group, queue, ^{NSLog(@"2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"down");});
在例子中,我把3個log分別放在并發隊列中,通過把這個并發隊列任務統一加入group中,group每次runloop的時候都會調用一個方法dispatch_group_wait(group, DISPATCH_TIME_NOW)
,用來檢查group中的任務是否已經完成,如果已經完成了,那么會執行dispatch_group_notify
的block,輸出'down'看一下運行結果:
2016-03-07 14:21:58.647 GCD[9424:156388] 2
2016-03-07 14:21:58.647 GCD[9424:156382] 0
2016-03-07 14:21:58.647 GCD[9424:156385] 1
2016-03-07 14:21:58.650 GCD[9424:156324] down
dispatch_barrier_async
此方法的作用是在并發隊列中,完成在它之前提交到隊列中的任務后打斷,單獨執行其block,并在執行完成之后才能繼續執行在他之后提交到隊列中的任務:
dispatch_queue_t concurrentDiapatchQueue=dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentDiapatchQueue, ^{NSLog(@"0");});
dispatch_async(concurrentDiapatchQueue, ^{NSLog(@"1");});
dispatch_async(concurrentDiapatchQueue, ^{NSLog(@"2");});
dispatch_async(concurrentDiapatchQueue, ^{NSLog(@"3");});
dispatch_barrier_async(concurrentDiapatchQueue, ^{sleep(1); NSLog(@"4");});
dispatch_async(concurrentDiapatchQueue, ^{NSLog(@"5");});
dispatch_async(concurrentDiapatchQueue, ^{NSLog(@"6");});
dispatch_async(concurrentDiapatchQueue, ^{NSLog(@"7");});
dispatch_async(concurrentDiapatchQueue, ^{NSLog(@"8");});
輸出的結果為:
2016-03-07 14:45:32.410 GCD[10079:169655] 1
2016-03-07 14:45:32.410 GCD[10079:169658] 2
2016-03-07 14:45:32.410 GCD[10079:169656] 0
2016-03-07 14:45:32.410 GCD[10079:169661] 3
2016-03-07 14:45:33.414 GCD[10079:169661] 4
2016-03-07 14:45:33.415 GCD[10079:169661] 5
2016-03-07 14:45:33.415 GCD[10079:169658] 6
2016-03-07 14:45:33.415 GCD[10079:169655] 8
2016-03-07 14:45:33.415 GCD[10079:169662] 7
4之后的任務在我線程sleep之后才執行,這其實就起到了一個線程鎖的作用,在多個線程同時操作一個對象的時候,讀可以放在并發進行,當寫的時候,我們就可以用dispatch_barrier_async
方法,效果杠杠的。
dispatch_sync
-
dispatch_sync
會在當前線程執行隊列,并且阻塞當前線程中之后運行的代碼,所以,同步線程非常有可能導致死鎖現象,我們這邊就舉一個死鎖的例子,直接在主線程調用以下代碼:
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"有沒有同步主線程?");
});
根據FIFO(先進先出)的原則,block里面的代碼應該在主線程此次runloop后執行,但是猶豫他是同步隊列,所有他之后的代碼會等待其執行完成后才能繼續執行,2者相互等待,所以就出現了死鎖。
我們再舉一個比較特殊的例子:
dispatch_queue_t queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{sleep(1);NSLog(@"1");});
dispatch_sync(queue, ^{sleep(1);NSLog(@"2");});
dispatch_sync(queue, ^{sleep(1);NSLog(@"3");});
NSLog(@"4");
其打印結果為:
2016-03-07 17:15:48.124 GCD[14198:272683] 1
2016-03-07 17:15:49.125 GCD[14198:272683] 2
2016-03-07 17:15:50.126 GCD[14198:272683] 3
2016-03-07 17:15:50.126 GCD[14198:272683] 4
從線程編號中我們發現,同步方法沒有去開新的線程,而是在當前線程中執行隊列,會有人問,上文說dispatch_get_global_queue不是并發隊列,并發隊列不是應該會在開啟多個線程嗎?這個前提是用異步方法。GCD其實是弱化了線程的管理,強化了隊列管理,這使我們理解變得比較形象。
dispatch_apply
這個方法用于無序查找,在一個數組中,我們能開啟多個線程來查找所需要的值,我這邊也舉個例子:
NSArray *array=[[NSArray alloc]initWithObjects:@"0",@"1",@"2",@"3",@"4",@"5",@"6", nil];
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(@"阻塞");
輸出結果:
2016-03-07 17:36:50.726 GCD[14318:291701] 1=1
2016-03-07 17:36:50.726 GCD[14318:291705] 0=0
2016-03-07 17:36:50.726 GCD[14318:291783] 3=3
2016-03-07 17:36:50.726 GCD[14318:291782] 2=2
2016-03-07 17:36:50.726 GCD[14318:291784] 5=5
2016-03-07 17:36:50.726 GCD[14318:291627] 4=4
2016-03-07 17:36:50.726 GCD[14318:291785] 6=6
2016-03-07 17:36:50.727 GCD[14318:291627] 阻塞
通過輸出log,我們發現這個方法雖然會開啟多個線程來遍歷這個數組,但是在遍歷完成之前會阻塞主線程。
dispatch_suspend & dispatch_resume
隊列掛起和恢復,這個沒什么好說的,直接上代碼:
dispatch_queue_t concurrentDiapatchQueue=dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentDiapatchQueue, ^{
for (int i=0; i<100; i++)
{
NSLog(@"%i",i);
if (i==50)
{
NSLog(@"-----------------------------------");
dispatch_suspend(concurrentDiapatchQueue);
sleep(3);
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_resume(concurrentDiapatchQueue);
});
}
}
});
我們甚至可以在不同的線程對這個隊列進行掛起和恢復,因為GCD是對隊列的管理。
Semaphore
我們可以通過設置信號量的大小,來解決并發過多導致資源吃緊的情況,以單核CPU做并發為例,一個CPU永遠只能干一件事情,那如何同時處理多個事件呢,聰明的內核工程師讓CPU干第一件事情,一定時間后停下來,存取進度,干第二件事情以此類推,所以如果開啟非常多的線程,單核CPU會變得非常吃力,即使多核CPU,核心數也是有限的,所以合理分配線程,變得至關重要,那么如何發揮多核CPU的性能呢?如果讓一個核心模擬傳很多線程,經常干一半放下干另一件事情,那效率也會變低,所以我們要合理安排,將單一任務或者一組相關任務并發至全局隊列中運算或者將多個不相關的任務或者關聯不緊密的任務并發至用戶隊列中運算,所以用好信號量,合理分配CPU資源,程序也能得到優化,當日常使用中,信號量也許我們只起到了一個計數的作用,真的有點大材小用。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);//為了讓一次輸出10個,初始信號量為10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i <100; i++)
{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//每進來1次,信號量-1;進來10次后就一直hold住,直到信號量大于0;
dispatch_async(queue, ^{
NSLog(@"%i",i);
sleep(2);
dispatch_semaphore_signal(semaphore);//由于這里只是log,所以處理速度非???,我就模擬2秒后信號量+1;
});
}
dispatch_once
這個函數一般是用來做一個真的單例,也是非常常用的,在這里就舉一個單例的例子吧:
static SingletonTimer * instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[SingletonTimer alloc] init];
});
return instance;
好了,blog說了這么多關于GCD中的方法,大家是不是覺得這篇blog并沒有什么高深的理論,本文更傾向于實用,看完這篇blog之后,大家一定對GCD躍躍欲試了吧!
參考文獻:《Objective-C高級編程 iOS與OS X多線程》