GCD是蘋果推出的一套從底層線程編程中抽象出的一種基于隊列來管理任務的方式。相對于直接在線程上執(zhí)行任務,使用GCD的方式更加簡便和高效。
通過GCD,調用者可以同步或者異步將要執(zhí)行的任務放進相應的隊列中等待執(zhí)行,隊列采用先進先出的方式取出里面的任務,然后將任務分配到相應的線程中執(zhí)行。
理解GCD需要理解幾個關鍵的概念和問題:
dispatch_async和dispatch_sync(異步執(zhí)行和同步執(zhí)行)
同步執(zhí)行和異步執(zhí)行是相對于當前調用的線程而言的。也就是說,dispatch_sync會阻塞當前的線程而dispatch_async不會。
并發(fā)隊列和串行隊列
串行隊列中的任務是一個個按順序完成的。后一個任務必須等待前一個任務完成。而并發(fā)隊列中的任務是可以同時進行的。同時進行意味者在并發(fā)隊列中同時進行的任務一定是在不同的線程中執(zhí)行的。
隊列和線程的關系
GCD提供了一個串行的主隊列和若干個全局并發(fā)隊列。主隊列中的所有任務都會在主線程中執(zhí)行,而全局并發(fā)隊列會根據應用當前執(zhí)行的情況來給任務分配線程,包括主線程。
重要:雖然下面會大量地提到線程,但是GCD的核心在于如何通過隊列的方式來思考和執(zhí)行相應的任務來達到想要的效果,大部分時候都可以忽略線程,但是在學習的時候我們可以多考慮一下,加深對GCD的理解
自己創(chuàng)建的隊列中的任務最終會進入到默認優(yōu)先級的全局隊列或者主隊列。關系可見官方的配圖:可以通過一些簡單的代碼來驗證一下上面的內容:
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent.async", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"Before excute: %@",[NSThread currentThread]);
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"async block 1: %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"async block 2: %@",[NSThread currentThread]);
});
NSLog(@"After excute: %@",[NSThread currentThread]);
運行結果如下:
2017-04-18 17:34:56.905 ConcurrentProgram[89385:39591640] Before excute: <NSThread: 0x6180000678c0>{number = 1, name = main}
2017-04-18 17:34:56.906 ConcurrentProgram[89385:39591640] After excute: <NSThread: 0x6180000678c0>{number = 1, name = main}
2017-04-18 17:34:58.911 ConcurrentProgram[89385:39593236] async block 2: <NSThread: 0x61800006c9c0>{number = 3, name = (null)}
2017-04-18 17:34:59.910 ConcurrentProgram[89385:39593020] async block 1: <NSThread: 0x60000006ffc0>{number = 4, name = (null)}
分析:Before excute和After excute都是在主線程中完成的。在兩者間主線程調用了兩次dispatch_async方法來將兩個任務,block1和block2放入自定義的并發(fā)隊列中。從運行結果看,先完后After Excute,說明主線程的執(zhí)行并沒有因為調用dispatch_async而阻塞。從時間上看,block2先于block1完成,并且二者相差一秒,剛好是兩者sleep的時間的差。說明這兩個任務是同時執(zhí)行的,這一點還可以在打印出來的線程地址不一致中得到印證。
過程如下:
- 主線程打印Before excute
- 主線程將block1放入自定義并發(fā)隊列中異步執(zhí)行,并發(fā)隊列開線程A執(zhí)行block1,
- 主線程繼續(xù)執(zhí)行,將block2放入并發(fā)隊列中異步執(zhí)行,并發(fā)隊列取出block2,再開辟新的線程B執(zhí)行block2
- 主線程繼續(xù)執(zhí)行,打印After excute
- B線程在休眠兩秒后打印
- A線程在休眠3秒后打印
再看下一個示例:
- (void)dispatchSyncFromMainToConcurrentQueue{
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent.sync", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"Before excute: %@",[NSThread currentThread]);
dispatch_sync(concurrentQueue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"async block 1: %@",[NSThread currentThread]);
});
dispatch_sync(concurrentQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"sync block 2: %@",[NSThread currentThread]);
});
NSLog(@"After excute: %@",[NSThread currentThread]);
}
結果如下:
2017-04-19 10:31:51.647 ConcurrentProgram[29439:40557792] Before excute: <NSThread: 0x61000006d840>{number = 1, name = main}
2017-04-19 10:31:54.648 ConcurrentProgram[29439:40557792] async block 1: <NSThread: 0x61000006d840>{number = 1, name = main}
2017-04-19 10:31:56.649 ConcurrentProgram[29439:40557792] sync block 2: <NSThread: 0x61000006d840>{number = 1, name = main}
2017-04-19 10:31:56.649 ConcurrentProgram[29439:40557792] After excute: <NSThread: 0x61000006d840>{number = 1, name = main}
過程如下:
- 主線程打印before excute
- 主線程將block1放入并發(fā)隊列中同步執(zhí)行,此時主線程阻塞
- 并發(fā)隊列取出block1,將block1分配給主線程執(zhí)行,block1打印
- block1執(zhí)行完畢后,主線程繼續(xù)執(zhí)行,將block2放入隊列中,
- 隊列將block2分配給主線程執(zhí)行,block2打印
- block2執(zhí)行完畢后主線程繼續(xù)執(zhí)行,打印after excute
結論:
- 放進同一個并發(fā)隊列中的不同任務不一定就會分配到不同的線程
- 并發(fā)隊列也可能將隊列中的任務交由主線程執(zhí)行
推測:在調用dispatch_sync時,由于調用的線程阻塞,或者在等待dispatch的任務完成之前是空閑的,那么dispatch的隊列很可能將該任務交由調用的線程執(zhí)行,這也是有些文章在說dispatch_sync和dispatch_async的區(qū)別是會說一個不會新開線程一個會新開線程,其實這個說法不正確。如果dispatch_sync或者dispatch_async的隊列參數是主隊列,那么一定會放進主線程中執(zhí)行,此時不存在開不開線程的問題。dispatch_sync和dispatch_async的根本區(qū)別還是在于是否會阻塞調用該方法的線程。
再看一下串行隊列的例子
- (void)serialQueue{
dispatch_queue_t serialQueue = dispatch_queue_create("serial", nil);
NSLog(@"Before excute: %@",[NSThread currentThread]);
dispatch_async(serialQueue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"async block 1: %@",[NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"sync block 2: %@",[NSThread currentThread]);
});
NSLog(@"After excute: %@",[NSThread currentThread]);
}
2017-04-19 10:58:56.296 ConcurrentProgram[30778:40656851] Before excute: <NSThread: 0x618000074740>{number = 1, name = main}
2017-04-19 10:58:59.297 ConcurrentProgram[30778:40657162] async block 1: <NSThread: 0x60000007ea40>{number = 3, name = (null)}
2017-04-19 10:59:01.298 ConcurrentProgram[30778:40656851] sync block 2: <NSThread: 0x618000074740>{number = 1, name = main}
2017-04-19 10:59:01.299 ConcurrentProgram[30778:40656851] After excute: <NSThread: 0x618000074740>{number = 1, name = main}
過程:
- 主線程打印before
- 主線程將block1放入串行隊列中異步執(zhí)行,串行隊列取出任務,開辟線程A執(zhí)行block1(由于主線程此時是不阻塞的,還要繼續(xù)執(zhí)行,因此必須新開一個線程執(zhí)行block1)
- 此時主線程繼續(xù)執(zhí)行,將block2放入串行隊列中同步執(zhí)行,主線程阻塞,等待block2執(zhí)行完畢。
- 由于是串行隊列,block2必須等待block1執(zhí)行完畢。
- 線程A執(zhí)行block1完畢,串行隊列取出block2并分配給主線程執(zhí)行并打?。ㄒ驗閎lock2執(zhí)行完之前主線程必須等待,讓主線程執(zhí)行完全沒有問題,省下開辟線程的開銷)
- 最后主線程打印aftert
一個常見的用法:
NSLog(@"Before excute: %@",[NSThread currentThread]);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//do some calcutate
[NSThread sleepForTimeInterval:3];
NSLog(@"block calculate: %@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"main block upadte: %@",[NSThread currentThread]);
});
});
NSLog(@"After excute: %@",[NSThread currentThread]);
在主線程的執(zhí)行過程中異步地將一個需要耗時的任務放入全局并發(fā)隊列,此時隊列會使用非主線程執(zhí)行該任務,等待任務完成后在將更新UI的任務放入主隊列,主隊列分配給主線程執(zhí)行更新UI的任務。
一個不要犯的錯誤:
dispatch_queue_t queue = dispatch_queue_create("wrong", nil);
NSLog(@"Before excute: %@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"block 1: %@",[NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"block2 %@",[NSThread currentThread]);
});
NSLog(@"after block2");
});
NSLog(@"After excute: %@",[NSThread currentThread]);
會造成EXEC_BAD_INSTRUCTION的錯誤。在官方文檔中有這樣一句提示:
Important: You should never call the dispatch_sync
or dispatch_sync_f
function from a task that is executing in the same queue that you are planning to pass to the function. This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.
永遠不要在一個隊列中調用dispatch_sync方法到同一個隊列中,尤其是在串行隊列(一定死鎖),在并發(fā)隊列中也應該避免使用。
來分析一下為什么:
串行隊列正在執(zhí)行任務A,中間調用了dispatch_async要分派一個新的任務B(block)給隊列自己,此時正在執(zhí)行的線程被阻塞等待任務B執(zhí)行,但是任務B在串行隊列中排在任務A后面需要等待A完成后執(zhí)行。于是B等待A,A等待B。死鎖。
并發(fā)隊列雖然沒有上述死鎖問題(因為并發(fā)隊列不需要等待上一個任務完成)。要避免這樣寫估計是習慣問題。因為大多數情況下dispatch到其他的隊列效果是一樣的而不會造成誤解。
dispatch_barrier_async
dispatch_barrier_async的意義在于,對于并發(fā)隊列而言,在某些時候可以像串行隊列一樣控制任務的執(zhí)行順序。
假設有這樣的需求,任務A執(zhí)行30s后得到結果A1,任務B需要執(zhí)行20s后得到結果B1,任務C的運算依賴于A1 和 B1,得到結果C1,任務D的處依賴于C1。那么我們要保證任務C在任務A和B執(zhí)行完之后,D在任務C執(zhí)行完以后才能有正確的結果。當然我們可以將ABCD放進一個串行隊列,但是如果A和B都是非常耗時的任務,那么會造成計算時間上的浪費。因為A和B是可以并發(fā)計算的。這個時候采用dispatch_barrier_async就非常有意義了。
dispatch_queue_t queue = dispatch_queue_create("My concurrent queue", DISPATCH_QUEUE_CONCURRENT);
__block int a1 = 0;
__block int b1 = 0;
__block int c1 = 0;
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:3];
a1 = 5;
NSLog(@"A finish");
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
b1 = 10;
NSLog(@"B finish");
});
dispatch_barrier_async(queue, ^{
c1 = a1+b1;
NSLog(@"handle A1 and B1");
});
dispatch_async(queue, ^{
NSLog(@"handle C1");
});
運行結果為:
2017-04-19 15:18:32.106 ConcurrentProgram[41116:41124691] B finish
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41124651] A finish
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41123629] handle A1 and B1
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41124651] handle C1
dispatch_barrier_async中的任務會等待其隊列中在它之前的任務完成后執(zhí)行,并且在該任務后面加入隊列的任務(如最后一個block)會等待該任務執(zhí)行完之后才會開始執(zhí)行。
這個API文檔中的discussion里有這樣一句話:
The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create
function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async
function.
也就是說這個API只有對于自己創(chuàng)建的并發(fā)隊列是有用的,否則就和dispatch_async效果是一樣的。
我們考慮一下如果是串行隊列,那么本來里面的任務就是必須按順序一個接一個完成,所以使用這個API是沒有意義的。問題在于為什么系統提供的全局并發(fā)隊列無法達到相應的效果呢?
個人猜測是這樣的:上面說過,自己創(chuàng)建的隊列中的任務最終會放入到系統的全局隊列中或者主隊列中,那么這個API可能只是控制了在什么時機將隊列中的任務放進全局隊列中去。采用剛剛的例子,就是自己創(chuàng)建的queue中有ABCD四個任務,queue將A和B先放入全局隊列,等待A和B都執(zhí)行完以后,將C放進全局隊列,C執(zhí)行完成后再將D放進全局隊列中。所以全局隊列中本身是沒有這種機制的。