昨天通過多線程實現(xiàn)方案之 -- NSThread說了關于 NSThread 多線程的一些知識點和用法, 其實之前我也寫過一篇關于 GCD 的分享-iOS - GCD 編程, 使用的 GCD 是基于封裝過的, 今天是深入學習總結 GCD 相關知識以及 GCD 在實際開發(fā)中的使用
什么是 GCD
- 全稱是 Grand Central Dispatch
- 純 C語言, 提供了非常強大的函數(shù)
GCD 的優(yōu)勢
- GCD 是蘋果公司為多核的并發(fā)運算提出的解決方案
- GCD 會自動利用更多的 CPU 內(nèi)核(比如雙核, 四核)
- GCD 會自動管理線程的生命周期, (創(chuàng)建線程, 調(diào)度任務, 銷毀線程)
- 程序員只需要告訴 GCD 需要執(zhí)行什么任務, 不需要編寫任何線程管理代碼
任務和隊列
GCD 中有兩個核心概念
- 任務: 執(zhí)行什么操作
- 隊列: 用來仿什么任務
GCD 使用的兩個步驟
-
定制任務
- 確定想要做的事情
-
講任務添加到隊列中
- GCD 會自動將隊列中的任務取出, 放到對應的線程中執(zhí)行
- 任務的取出遵循隊列的 FIFO 原則: 先進先出, 后進后出
執(zhí)行任務
GCD 中有兩個用來執(zhí)行任務的常用函數(shù)
- 用同步的方式執(zhí)行任務
dispatch_sync(dispatch_queue_t queue, ^{
// block 內(nèi)容
});
queue: 隊列
bolck: 任務
- 用異步方式來執(zhí)行
dispatch_async(dispatch_queue_t queue, ^{
// block 內(nèi)容
});
同步和異步的區(qū)別
- 同步: 只能在當前線程中執(zhí)行任務, 不具備開新啟線程的能力
- 異步: 可以在新的線程中執(zhí)行任務, 具備開啟新線程的能力
隊列的類型
并發(fā)隊列(Concurrent Dispatch Queue)
概念:
- 可以讓多個任務并發(fā)(同時)執(zhí)行(自動開啟多個線程同時執(zhí)行任務)
- 并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效
創(chuàng)建方法:
dispatch_queue_t queue = dispatch_queue_create("abc", DISPATCH_QUEUE_CONCURRENT);
因為 GCD 默認已經(jīng)提供了全局并發(fā)隊列, 供整個應用使用, 也可以直接獲取
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 第一個參數(shù)是隊列優(yōu)先級, 第二個參數(shù)一般都是0, 沒什么用
串行隊列(Serial Dispatch Queue)
概念:
- 讓任務一個接著一個的執(zhí)行 (一個任務執(zhí)行完畢再執(zhí)行下一個任務)
創(chuàng)建方式:
// 第二個隊列類型可以傳 NULL 或者 DISPATCH_QUEUE_SERIAL 效果是一樣的
dispatch_queue_t queue = dispatch_queue_create("abc", DISPATCH_QUEUE_SERIAL);
還可以使用主隊列, 也就是跟主線程想關聯(lián)的隊列
- 主隊列是 GCD 自帶的一種特殊串行隊列
- 放在主隊列中的任務, 多會放到主線程中執(zhí)行
- 使用
dispatch_get_main_queue()
獲得主隊列
容易混淆的術語
在 GCD 使用的時候有4個概念比較容易混淆: 同步 - 異步 - 并發(fā) - 串行
-
同步和異步主要影響: 能不能開啟新線程
- 同步: 只是在當前線程中執(zhí)行任務, 不具備開啟新線程的能力
- 異步: 可以在新的線程中執(zhí)行任務, 具備開啟新線程的能力
-
并發(fā)和串行主要影響: 任務的執(zhí)行方式
- 并發(fā): 允許多個任務并發(fā)執(zhí)行
- 串行: 一個任務執(zhí)行完畢, 才去執(zhí)行下一個任務
GCD 的基本使用
同步函數(shù)和并發(fā)隊列
直接上代碼
- (void)syncConcurrent {
// 創(chuàng)建隊列
/*
第一個參數(shù): C語言的字符串,標簽
第二個參數(shù): 隊列的類型
*/
dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);
// 定制任務(多任務)
dispatch_sync(queue, ^{
NSLog(@"download1- %@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download2- %@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download3- %@", [NSThread currentThread]);
});
}
打印結果:
2016-07-28 11:14:52.766 GCD[18620:1367714] download1- <NSThread: 0x7f9ee8e014b0>{number = 1, name = main}
2016-07-28 11:14:52.767 GCD[18620:1367714] download2- <NSThread: 0x7f9ee8e014b0>{number = 1, name = main}
2016-07-28 11:14:52.770 GCD[18620:1367714] download3- <NSThread: 0x7f9ee8e014b0>{number = 1, name = main}
同步函數(shù)是不會開啟子線程的, 所有任務都是在主線程中串行執(zhí)行的.
同步函數(shù)和串行隊列
代碼:
- (void)syncSerial {
dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"download1- %@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download2- %@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download3- %@", [NSThread currentThread]);
});
}
打印結果:
2016-07-28 11:18:56.510 GCD[18829:1370325] download1- <NSThread: 0x7fe08bf00af0>{number = 1, name = main}
2016-07-28 11:18:56.511 GCD[18829:1370325] download2- <NSThread: 0x7fe08bf00af0>{number = 1, name = main}
2016-07-28 11:18:56.513 GCD[18829:1370325] download3- <NSThread: 0x7fe08bf00af0>{number = 1, name = main}
同樣, 此時也是不會創(chuàng)建子線程的, 所有任務是在主線程中也是串行執(zhí)行, 和同步函數(shù)和并發(fā)隊列時候是一樣的效果.
同步函數(shù)和主隊列
這個就有點特殊了, 為了看效果, 在方法里面加上開始和結束的代碼:
- (void)syncMain {
NSLog(@"---start---");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"download1- %@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download2- %@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download3- %@", [NSThread currentThread]);
});
NSLog(@"---end---");
}
打印結果:
2016-07-28 12:02:20.473 GCD[21183:1395248] ---start---
并沒有打印結束語句, 說明任務也沒有執(zhí)行,這是怎么回事呢?
- 這是因為主列發(fā)中的任務都是在主線程中執(zhí)行, 當主隊列發(fā)現(xiàn)當前主線程有任務在執(zhí)行, 那主隊列會暫定調(diào)用對隊列中的任務,直到主線程空閑為止.
- 簡單點說, 就是主線程發(fā)現(xiàn)有任務, 就要讓主線程去執(zhí)行任務, 但此時的主線程卻在等待這任務執(zhí)行完畢, 不是空閑狀態(tài), 所以主線程無法執(zhí)行任務, 形成死鎖. 而同步函數(shù)又要求任務要立刻馬上按順序執(zhí)行, 所以第一個任務執(zhí)行不了, 后面的當然也執(zhí)行不了 , 就卡在了那里.
那有沒有辦法讓同步函數(shù)和主隊列中的任務執(zhí)行呢? 當然可以, 只是需要把這個方法放到子線程中去, 看代碼:
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
這個時候再看執(zhí)行結果:
2016-07-28 12:17:01.477 GCD[21971:1403347] ---start---
2016-07-28 12:17:01.481 GCD[21971:1403123] download1- <NSThread: 0x7fca2bc02470>{number = 1, name = main}
2016-07-28 12:17:01.500 GCD[21971:1403123] download2- <NSThread: 0x7fca2bc02470>{number = 1, name = main}
2016-07-28 12:17:01.503 GCD[21971:1403123] download3- <NSThread: 0x7fca2bc02470>{number = 1, name = main}
2016-07-28 12:17:01.504 GCD[21971:1403347] ---end---
發(fā)現(xiàn)已經(jīng)全部執(zhí)行完畢了, 而且是在主線程中執(zhí)行的. 這是因為我們是開啟的子線程來調(diào)用方法, 此時的主線程是空閑的, 然后方法中的任務需要在主線程中執(zhí)行, 就沒有問題了.
異步函數(shù)和并發(fā)隊列
定制三個任務, 看執(zhí)行效果
- (void)asyncConcurrent {
dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);
// 也可以獲取全局并發(fā)隊列,執(zhí)行效果是一樣的
// dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"download1- %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download2- %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download3- %@", [NSThread currentThread]);
});
}
打印結果:
2016-07-28 10:58:05.941 GCD[17694:1357547] download1- <NSThread: 0x7f9c5b6155e0>{number = 2, name = (null)}
2016-07-28 10:58:05.943 GCD[17694:1357551] download3- <NSThread: 0x7f9c5b551df0>{number = 4, name = (null)}
2016-07-28 10:58:05.942 GCD[17694:1357550] download2- <NSThread: 0x7f9c5b548b30>{number = 3, name = (null)}
可以看出隊列開啟了三條子線程區(qū)分別執(zhí)行三個任務, 隊列中的任務是并發(fā)執(zhí)行的. 但是在這里有個注意點:
并不是說有多少任務GCD 就會開啟多少條線程, 具體開啟幾條線程是不確定的, 這個是由系統(tǒng)決定的.
異步函數(shù)和串行隊列
同樣是三個任務,看執(zhí)行效果
- (void)asyncSerial {
dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"download1- %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download2- %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download3- %@", [NSThread currentThread]);
});
}
打印結果
2016-07-28 11:08:08.933 GCD[18241:1363508] download1- <NSThread: 0x7f80c0f11330>{number = 2, name = (null)}
2016-07-28 11:08:08.934 GCD[18241:1363508] download2- <NSThread: 0x7f80c0f11330>{number = 2, name = (null)}
2016-07-28 11:08:08.934 GCD[18241:1363508] download3- <NSThread: 0x7f80c0f11330>{number = 2, name = (null)}
隊列只開啟了一條子線程, 去一個接著一個任務去執(zhí)行.
這種方式對任務的執(zhí)行效率沒有任何提高.
異步函數(shù)和主隊列
代碼:
- (void)asyncMain {
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"download1- %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download2- %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download3- %@", [NSThread currentThread]);
});
}
打印結果:
2016-07-28 11:55:12.710 GCD[20775:1389553] download1- <NSThread: 0x7fc4ea7033b0>{number = 1, name = main}
2016-07-28 11:55:12.712 GCD[20775:1389553] download2- <NSThread: 0x7fc4ea7033b0>{number = 1, name = main}
2016-07-28 11:55:12.712 GCD[20775:1389553] download3- <NSThread: 0x7fc4ea7033b0>{number = 1, name = main}
主隊列所有的任務確實是在主線程執(zhí)行的, 雖然是異步函數(shù), 但也不會開啟線程.
各種隊列執(zhí)行效果總結
直接在 Excel 里做了個表
總結: GCD 里, 非主隊列情況下只有異步函數(shù)才會開啟新線程, 此時如果是并發(fā)隊列, 會開啟多條線程,如果是串行隊列, 只會開啟一條線程, 其他情況下(包括主隊列) 都不會開啟新線程,并且是串行執(zhí)行任務.
GCD 線程間通信
GCD 線程間通信相對來說是比較簡單的, 直接使用嵌套就可以了.
// 開啟子線程下載圖片
// dispatch_sync 和 dispatch_async 兩者效果一樣,因為是在子線程下載的
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 網(wǎng)絡圖片 url
NSURL *url = [NSURL URLWithString:@"http://pic12.nipic.com/20110114/6621051_221433460330_2.jpg"];
// 下載二進制數(shù)據(jù)到本地
NSData *data = [NSData dataWithContentsOfURL:url];
// 獲取圖片
UIImage *image = [[UIImage alloc] initWithData:data];
// 回到主線程刷新 UI 圖片
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
這樣就能實現(xiàn)在子線程下載圖片,回到主線程刷新 UI 并設置圖片
GCD 常用函數(shù)
delay 延遲操作
先看前兩種方法
NSLog(@"-----start-----");
// 延遲方法 第一種
[self performSelector:@selector(task) withObject:nil afterDelay:3.0];
// 第二種
//[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
方法實現(xiàn):
- (void)task {
NSLog(@"task-%@", [NSThread currentThread]);
}
打印結果是一樣的
2016-07-28 13:27:16.779 GCD 常用函數(shù)[25917:1453136] -----start-----
2016-07-28 13:27:19.782 GCD 常用函數(shù)[25917:1453136] task-<NSThread: 0x7fa1ca604cf0>{number = 1, name = main}
只不過 NSTimer 會循環(huán)打印
用 GCD 會更簡單一些
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"GCD-%@", [NSThread currentThread]);
});
不需要額外寫其他方法, 在 block 里直接聲明要執(zhí)行的任務就可以了.
2016-07-28 13:30:20.043 GCD 常用函數(shù)[26131:1456661] -----start-----
2016-07-28 13:30:23.342 GCD 常用函數(shù)[26131:1456661] GCD-<NSThread: 0x7fe4687013f0>{number = 1, name = main}
也能達到延遲操作的作用, 此時是默認在主線程中執(zhí)行的 . GCD 可以修改任務任務執(zhí)行所在的線程.
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), queue, ^{
NSLog(@"GCD-%@", [NSThread currentThread]);
});
此時的執(zhí)行效果
2016-07-28 13:33:57.186 GCD 常用函數(shù)[26332:1459149] -----start-----
2016-07-28 13:34:00.188 GCD 常用函數(shù)[26332:1459321] GCD-<NSThread: 0x7f90c37146b0>{number = 2, name = (null)}
可以看到任務是在子線程中執(zhí)行的.
once 一次性執(zhí)行
直接上代碼
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"once - %@", [NSThread currentThread]);
});
打印結果:
2016-07-28 13:37:36.106 GCD 常用函數(shù)[26532:1461714] once - <NSThread: 0x7fead2e01670>{number = 1, name = main}
之后就不會再運行了, 它是在整個運行程序中只會執(zhí)行一次, GCD 的一次性執(zhí)行代碼一般都是用在單例設計模式中.保證全局只有一個對象實例.
GCD 柵欄函數(shù)
在異步函數(shù)中控制任務執(zhí)行的順序, 只有當柵欄函數(shù)執(zhí)行完畢之后才會執(zhí)行后面的任務.
- 注意:柵欄函數(shù)不能使用全局并發(fā)隊列
依然不善表達,直接上代碼, 為了能看出效果, 讓每個任務都執(zhí)行10次:
// 創(chuàng)建并發(fā)隊列
dispatch_queue_t queue = dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT);
// 異步函數(shù)
dispatch_async(queue, ^{
for (int i = 0; i < 10; ++i) {
NSLog(@"download1 - %@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; ++i) {
NSLog(@"download2 - %@", [NSThread currentThread]);
}
});
// 柵欄函數(shù)
dispatch_barrier_async(queue, ^{
NSLog(@"++++++++++++++++++++++++++++++++++++++++");
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; ++i) {
NSLog(@"download3 - %@", [NSThread currentThread]);
}
});
執(zhí)行結果:
只有在柵欄函數(shù)前面的任務全部執(zhí)行完畢后, 才會執(zhí)行后面的任務.
GCD 的 apply (快速迭代)
迭代: 也就是 遍歷
之前我們用的最多的就是 for 循環(huán)區(qū)遍歷10000次任務, 接下來我們就對比一下兩者有什么區(qū)別, 為了看出效果, 都加上耗時計算
首先是 for 循環(huán)
NSDate* tmpStartData = [NSDate date];
for (int i = 0; i < 10000; ++i) {
NSLog(@"for- %d -- %@", i, [NSThread currentThread]);
}
double deltaTime = [[NSDate date] timeIntervalSinceDate:tmpStartData];
NSLog(@"for 耗時 = %f", deltaTime);
運行結果:
for 循環(huán)用時約 10.5秒, 而且全部是在主線程中執(zhí)行的
接著用 GCD 的快速迭代
NSDate* tmpStartData = [NSDate date];
/*
第一個參數(shù): 迭代次數(shù)
第二個參數(shù): 線程隊列(并發(fā)隊列)
第三個參數(shù): index 索引
*/
dispatch_apply(10000, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"GCD- %zd -- %@", index, [NSThread currentThread]);
});
double deltaTime = [[NSDate date] timeIntervalSinceDate:tmpStartData];
NSLog(@"GCD 耗時 = %f", deltaTime);
運行結果
從上面兩張圖可以看出, GCD 快速迭代的是開啟了子線程去執(zhí)行的,而且主線程也參與了, 由于不是一個線程, 所以迭代也不是按順序的. 最后,用時5.08秒, 明顯快于 for 循環(huán)遍歷.
GCD 隊列組
隊列組的作用: 當執(zhí)行隊列組通知模塊時能保證放進隊列組里的任務全部執(zhí)行完畢了(之前那篇iOS - GCD 編程里也有類似介紹,不過那個是對 GCD 封裝過的方法)
```
// 創(chuàng)建隊列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 創(chuàng)建隊列組
dispatch_group_t group = dispatch_group_create();
//隊列組異步函數(shù)執(zhí)行任務
dispatch_group_async(group, queue, ^{
NSLog(@"任務1 -- %@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"任務2 -- %@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"任務3 -- %@", [NSThread currentThread]);
});
// 隊列組攔截通知模塊(內(nèi)部本身是異步執(zhí)行的,不會阻塞線程)
dispatch_group_notify(group, queue, ^{
NSLog(@"------隊列租任務執(zhí)行完畢-------");
});
```
執(zhí)行效果:
- 關于 GCD 的相關知識點基本總結完畢, 下篇文章接著總結 NSOperation 的相關知識點
相關文章:
iOS 多線程知識點總結之: 進程和線程
iOS 多線程實現(xiàn)方案之 -- NSThread
iOS - GCD 編程