一、介紹
GCD,英文全稱是Grand Central Dispatch(功能強悍的中央調度器),基于C語言編寫的一套多線程開發(fā)機制,因此使用時會以函數(shù)形式出現(xiàn),且大部分函數(shù)以dispatch開頭,雖然是C語言的但相對于蘋果其它多線程實現(xiàn)方式,抽象層次更高,使用起來也更加方便。
它是蘋果為應對多核的并行運算提出的解決方案,它會自動利用多核進行并發(fā)處理和運算,它能提供系統(tǒng)級別的處理,而不再局限于某個進程、線程,官方聲稱會更快、更高效、更靈敏,且線程由系統(tǒng)自動管理(調度、運行),無需程序員參與,使用起來非常方便。
二、任務和隊列
GCD有兩個核心:任務和隊列。
任務:要執(zhí)行的操作或方法函數(shù),隊列:存放任務的集合,而我們要做的就是將任務添加到隊列然后執(zhí)行,GCD會自動將隊列中的任務按先進先出的方式取出并交給對應線程執(zhí)行。注意任務的取出是按照先進先出的方式,這也是隊列的特性,但是取出后的執(zhí)行順序則不一定,下面會詳細討論。
1 任務
任務是一個比較抽象的概念,可以簡單的認為是一個操作、一個函數(shù)、一個方法等等,在實際的開發(fā)中大多是以block(block使用詳見)的形式,使用起來也更加靈活。
2 隊列queue
- 有兩種隊列:串行隊列和并行隊列。串行隊列:同步執(zhí)行,在當前線程執(zhí)行;并行隊列:可由多個線程異步執(zhí)行,但任務的取出還是FIFO的
隊列創(chuàng)建,根據(jù)函數(shù)第二個參數(shù)來創(chuàng)建串行或并行隊列。
// 參數(shù)1 隊列名稱
// 參數(shù)2 隊列類型 DISPATCH_QUEUE_SERIAL/NULL串行隊列,DISPATCH_QUEUE_CONCURRENT代表并行隊列
// 下面代碼為創(chuàng)建一個串行隊列,也是實際開發(fā)中用的最多的
dispatch_queue_t serialQ = dispatch_queue_create("隊列名", NULL);
- 另外系統(tǒng)提供了兩種隊列:全局隊列和主隊列。
全局隊列屬于并行隊列,只不過已由系統(tǒng)創(chuàng)建的沒有名字,且在全局可見(可用)。獲取全局隊列:
/* 取得全局隊列
第一個參數(shù):線程優(yōu)先級,設為默認即可,個人習慣寫0,等同于默認
第二個參數(shù):標記參數(shù),目前沒有用,一般傳入0
*/
serialQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
主隊列屬于串行隊列,也由系統(tǒng)創(chuàng)建,只不過運行在主線程(UI線程)。獲取主隊列:
// 獲取主隊列
serialQ = dispatch_get_main_queue();
- 關于內存:queue屬于一個對象,也是占用內存的,也會使用引用計數(shù),當向queue添加一個任務時就會將這個queue retain一下,引用計數(shù)+1,直到所有任務都完成內存才會釋放。(我們在聲明一個queue屬性時要用strong)。
3 執(zhí)行方式——2種
同步執(zhí)行和異步執(zhí)行。
- 同步執(zhí)行:不會開啟新的線程,在當前線程執(zhí)行。
- 異步執(zhí)行:gcd管理的線程池中有空閑線程就會從隊列中取出任務執(zhí)行,會開啟線程。
下面為實現(xiàn)同步和異步的函數(shù),函數(shù)功能為:將任務添加到隊列并執(zhí)行。
/* 同步執(zhí)行
第一個參數(shù):執(zhí)行任務的隊列:串行、并行、全局、主隊列
第二個參數(shù):block任務
*/
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 異步執(zhí)行
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
注意:默認情況下,新線程都沒有開啟runloop,所以當block任務完成后,線程都會自動被回收,假設我們想在新開的線程中使用NSTimer,就必須開啟runloop,可以使用[[NSRunLoop currentRunLoop] run]開啟當前線程,這是就要自己管理線程的回收等工作。
- 另外還有兩個方法,實際開發(fā)中用的并不是太多
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
和
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
加了一個barrier,意義在于:隊列之前的block處理完成之后才開始處理隊列中barrier的block,且barrier的block必須處理完之后,才能處理其它的block。
根據(jù)這個特性我們可以實現(xiàn)123456一共6個block,可以讓特定幾個并發(fā)執(zhí)行完成之后,再并發(fā)執(zhí)行剩下的block。比如123先并發(fā),之后456再并發(fā)執(zhí)行。具體代碼如下(將barrier放在123與456之間即可):
- (void)barrierTest {
// 1 創(chuàng)建并發(fā)隊列
dispatch_queue_t BCqueue = dispatch_queue_create("BarrierConcurrent", DISPATCH_QUEUE_CONCURRENT);
// 2.1 添加任務123
dispatch_async(BCqueue, ^{
NSLog(@"task1,%@", [NSThread currentThread]);
});
dispatch_async(BCqueue, ^{
sleep(3);
NSLog(@"task2,%@", [NSThread currentThread]);
});
dispatch_async(BCqueue, ^{
sleep(1);
NSLog(@"task3,%@", [NSThread currentThread]);
});
// 2.2 添加barrier
dispatch_barrier_async(BCqueue, ^{
NSLog(@"barrier");
});
// 2.3 添加任務456
dispatch_async(BCqueue, ^{
sleep(1);
NSLog(@"task4,%@", [NSThread currentThread]);
});
dispatch_async(BCqueue, ^{
NSLog(@"task5,%@", [NSThread currentThread]);
});
dispatch_async(BCqueue, ^{
NSLog(@"task6,%@", [NSThread currentThread]);
});
}
輸出結果,為了顯示效果,代碼有延時操作:

三、幾種類型
很明顯兩種執(zhí)行方式,兩種隊列。那么就有4種情況:串行隊列同步執(zhí)行、串行隊列異步執(zhí)行、并行隊列同步執(zhí)行、并行隊列異步執(zhí)行。哪一種會開啟新的線程?開幾條?是否并發(fā)?記憶起來比較繞,但是只要抓住基本的就可以,為了方便理解,現(xiàn)分析如下:
- 串行隊列,同步執(zhí)行-----串行隊列意味著順序執(zhí)行,同步執(zhí)行意味著不開啟線程(在當前線程執(zhí)行)
- 串行隊列,異步執(zhí)行-----串行隊列意味著任務順序執(zhí)行,異步執(zhí)行說明要開線程, (如果開多個線程的話,不能保證串行隊列順序執(zhí)行,所以只開一個線程)
- 并行隊列,異步執(zhí)行-----并行隊列意味著執(zhí)行順序不確定,異步執(zhí)行意味著會開啟線程,而并行隊列又允許不按順序執(zhí)行,所以系統(tǒng)為了提高性能會開啟多個線程,來隊列取任務(隊列中任務取出仍然是順序取出的,只是線程執(zhí)行無序)。
- 并行隊列,同步執(zhí)行-----同步執(zhí)行意味著不開線程,則肯定是順序執(zhí)行
- 死鎖-----程序執(zhí)行不出來(死鎖) ;
四、死鎖舉例
- 主隊列死鎖:
這種死鎖最常見,問題也最嚴重,會造成主線程卡住。原因:主隊列,如果主線程正在執(zhí)行代碼,就不調度任務;同步執(zhí)行:一直執(zhí)行第一個任務直到結束。兩者互相等待造成死鎖,示例如下:
- (void)mainThreadDeadLockTest {
NSLog(@"begin");
dispatch_sync(dispatch_get_main_queue(), ^{
// 發(fā)生死鎖下面的代碼不會執(zhí)行
NSLog(@"middle");
});
// 發(fā)生死鎖下面的代碼不會執(zhí)行,當然函數(shù)也不會返回,后果也最為嚴重
NSLog(@"end");
}
- 在其它線程死鎖,這種不會影響主線程:
原因:serialQueue為串行隊列,當代碼執(zhí)行到block1時正常,執(zhí)行到dispatch_sync時,dispatch_sync等待block2執(zhí)行完畢才會返回,而serialQueue是串行隊列,它正在執(zhí)行block1,只有等block1執(zhí)行完畢后才會去執(zhí)行block2,相互等待造成死鎖
- (void)deadLockTest {
// 其它線程的死鎖
dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
// 串行隊列block1
NSLog(@"begin");
dispatch_sync(serialQueue, ^{
// 串行隊列block2 發(fā)生死鎖,下面的代碼不會執(zhí)行
NSLog(@"middle");
});
// 不會打印
NSLog(@"end");
});
// 函數(shù)會返回,不影響主線程
NSLog(@"return");
}
五、常用舉例
- 線程間通訊
比如,為了提高用戶體驗,我們一般在其他線程(非主線程)下載圖片或其它網絡資源,下載完成后我們要更新UI,而UI更新必須在主線程執(zhí)行,所以我們經常會使用:
// 同步執(zhí)行,會阻塞指導下面block中的代碼執(zhí)行完畢
dispatch_sync(dispatch_get_main_queue(), ^{
// 主線程,UI更新
});
// 異步執(zhí)行
dispatch_async(dispatch_get_main_queue(), ^{
// 主線程,UI更新
});
- 信號量的使用
也屬于線程間通訊,下面的舉例是經常用到的場景。在網絡訪問中,NSURLSession類都是異步的(找了很久沒有找到同步的方法),而有時我們希望能夠像NSURLConnection一樣可以同步訪問,即在網絡block調用完成之后做一些操作。那我們可以使用dispatch的信號量來解決:
/// 用于線程間通訊,下面是等待一個網絡完成
- (void)dispatchSemaphore {
NSString *urlString = [@"https://www.baidu.com" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
// 設置緩存策略為每次都從網絡加載 超時時間30秒
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 處理完成之后,發(fā)送信號量
NSLog(@"正在處理...");
dispatch_semaphore_signal(semaphore);
}] resume];
// 等待網絡處理完成
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"處理完成!");
}
在上面的舉例中
dispatch_semaphore_signal的調用必須是在另一個線程調用,因為當前線程已經dispatch_semaphore_wait阻塞。另外,dispatch_semaphore_wait最好不要在主線程調用
- 全局隊列,實現(xiàn)并發(fā):
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 要執(zhí)行的代碼
});
六、Dispatch Group調度組
使用調度組,可以輕松實現(xiàn)在一些任務完成后,做一些操作。比如具有順序性要求的生產者消費者等等。
- 示例1:任務1完成之后執(zhí)行任務2。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self groupTest];
}
- (void)groupTest {
// 創(chuàng)建一個組
dispatch_group_t group = dispatch_group_create();
NSLog(@"開始執(zhí)行");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 任務1
// 等待1s一段時間在執(zhí)行
[NSThread sleepForTimeInterval:1];
NSLog(@"task1 running in %@",[NSThread currentThread]);
});
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
// 任務2
NSLog(@"task2 running in %@",[NSThread currentThread]);
});
});
}
點擊屏幕后,打印如下,可以看到任務1雖然等待了1s,任務2也不執(zhí)行,只有任務1執(zhí)行完畢才執(zhí)行任務2.
2015-08-28 18:16:05.317 GCDTest[1468:229374] 開始執(zhí)行
2015-08-28 18:16:06.323 GCDTest[1468:229457] task1 running in <NSThread: 0x7f8962f16900>{number = 2, name = (null)}
2015-08-28 18:16:06.323 GCDTest[1468:229456] task2 running in <NSThread: 0x7f8962c92750>{number = 3, name = (null)}
- 示例2:其實示例1并不常用,真正用到的是監(jiān)控多個任務完成之后,回到主線程更新UI,或者做其它事情。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self groupTest];
}
- (void)groupTest {
// 創(chuàng)建一個組
dispatch_group_t group = dispatch_group_create();
NSLog(@"開始執(zhí)行");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 關聯(lián)任務1
NSLog(@"task1 running in %@",[NSThread currentThread]);
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 關聯(lián)任務2
NSLog(@"task2 running in %@",[NSThread currentThread]);
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 關聯(lián)任務3
NSLog(@"task3 running in %@",[NSThread currentThread]);
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 關聯(lián)任務4
// 等待1秒
[NSThread sleepForTimeInterval:1];
NSLog(@"task4 running in %@",[NSThread currentThread]);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 回到主線程執(zhí)行
NSLog(@"mainTask running in %@",[NSThread currentThread]);
});
});
}
點擊屏幕后,打印如下,可以看到無論其它任務然后和執(zhí)行,mainTask等待它們執(zhí)行后才執(zhí)行。
2015-08-28 18:24:14.312 GCDTest[1554:236273] 開始執(zhí)行
2015-08-28 18:24:14.312 GCDTest[1554:236352] task3 running in <NSThread: 0x7fa8f1f0c9c0>{number = 4, name = (null)}
2015-08-28 18:24:14.312 GCDTest[1554:236354] task1 running in <NSThread: 0x7fa8f1d10750>{number = 2, name = (null)}
2015-08-28 18:24:14.312 GCDTest[1554:236351] task2 running in <NSThread: 0x7fa8f1c291a0>{number = 3, name = (null)}
2015-08-28 18:24:15.313 GCDTest[1554:236353] task4 running in <NSThread: 0x7fa8f1d0e7f0>{number = 5, name = (null)}
2015-08-28 18:24:15.313 GCDTest[1554:236273] mainTask running in <NSThread: 0x7fa8f1c13df0>{number = 1, name = main}
關于Dispatch對象內存管理問題
根據(jù)上面的代碼,可以看出有關dispatch的對象并不是OC對象,那么,用不用像對待Core Foundation框架的對象一樣,使用retain/release來管理呢?答案是不用的!
如果是ARC環(huán)境,我們無需管理,會像對待OC對象一樣自動內存管理。
如果是MRC環(huán)境,不是使用retain/release,而是使用dispatch_retain/dispatch_release來管理。