OS X 和iOS 中的多線程技術(下)
上篇文章中介紹了 pthread 和 NSThread 兩種多線程的方式,本文將繼續(xù)介紹 GCD 和 NSOperation 這兩種方式。。
1.GCD
1.1 什么是GCD
- GCD 全稱 Grand Central Dispatch,可譯為“牛逼的中樞調度器”
- GCD 基于純 C 語言,內部封裝了強大的函數庫
1.2 使用 GCD 有什么優(yōu)勢
- GCD 是蘋果公司為多核的并行運算提出的解決方案
- GCD 會自動利用更多的CPU內核 (如 二核 ,四核)
- GCD 會自動管理線程的生命周期(創(chuàng)建 、 調度 、 銷毀線程)
- 程序員只需要告訴 GCD 想要執(zhí)行什么任務,不需要編寫任何線程管理代碼
1.3 GCD 的使用
-
GCD 有兩個核心的概念
- 任務 : 需要執(zhí)行的操作
- 隊列 : 用來存放任務
-
GCD 的使用步驟
- 制定任務
- 將任務放入到隊列中,GCD會自動將隊列中的任務取出,放到對應的線程中執(zhí)行,隊列中的任務取出遵循 FIFO原則。(FIFO:先進先出,隊列原則)
-
GCD 中有兩個用來執(zhí)行任務的常用函數
-
同步方法執(zhí)行任務
dispatch_sync(dispatch_queue_t _Nonnull queue, ^(void)block) queue : 隊列 Block : 任務 -
異步方法執(zhí)行任務
dispatch_async(dispatch_queue_t _Nonnull queue, ^(void)block)
-
-
同步和異步的區(qū)別
- 同步 : 只能在當前的線程中執(zhí)行任務,不具備開啟新線程的能力
- 異步 : 可以在新的線程中執(zhí)行任務,具備開啟新線程的能力
1.4 隊列的類型
GCD 的隊列可以分為 2 大類
-
并發(fā)隊列 ( Concurrent Dispatch Queue )
- 可以讓多任務并發(fā)執(zhí)行,自動開啟多個線程同時執(zhí)行任務
- 并發(fā)功能只有在異步(dispatch_async)函數下才有效
-
串行隊列 ( Serial Dispatch Queue )
- 讓任務一個接一個地有序執(zhí)行(一個任務執(zhí)行完畢后才開始執(zhí)行下一個)
注意:同步 、 異步、并發(fā)、串行的區(qū)分
-
同步和異步主要影響: 能不能開啟新的線程- 同步 : 只是在當前線程中執(zhí)行任務 ,不具備開啟新線程的能力
- 異步 : 可以在新的線程中執(zhí)行任務,具備開啟新縣城的能力
-
并發(fā)和串行主要影響: 任務的執(zhí)行方式- 并發(fā) : 多個任務并發(fā)執(zhí)行
- 串行 : 多個任務一次順序執(zhí)行
1.5 GCD 的各種隊列的組合
- 異步函數 + 并發(fā)隊列:可以同時開啟多條線程
// 1.獲得全局的并發(fā)隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2.將任務加入隊列
dispatch_async(queue, ^{
for (NSInteger i = 0; i<10; i++) {
NSLog(@"1-----%@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<10; i++) {
NSLog(@"2-----%@", [NSThread currentThread]);
}
});
- 同步函數 + 并發(fā)隊列:不會開啟新的線程
// 1.獲得全局的并發(fā)隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2.將任務加入隊列
dispatch_sync(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
- 異步函數 + 串行隊列:會開啟新的線程,但是任務是串行的,執(zhí)行完一個任務,再執(zhí)行下一個任務
// 1.創(chuàng)建串行隊列
dispatch_queue_t queue = dispatch_queue_create("com.coder.queue", DISPATCH_QUEUE_SERIAL);
// dispatch_queue_t queue = dispatch_queue_create("com.coder.queue", NULL);
// 2.將任務加入隊列
dispatch_async(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
- 異步函數 + 主隊列:只在主線程中執(zhí)行任務
// 1.創(chuàng)建串行隊列
dispatch_queue_t queue = dispatch_queue_create("com.coder.queue", DISPATCH_QUEUE_SERIAL);
// 2.將任務加入隊列
dispatch_sync(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
- 同步函數 + 主隊列:
// 1.獲得主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 2.將任務加入隊列
dispatch_sync(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
各種隊列的執(zhí)行效果 :

注意:
使用 sync 函數往當前串行隊列中添加任務,會卡住當前的串行隊列
1.6 GCD 個線程之間通信
通常開辟子線程是為了執(zhí)行耗時操作。如下載圖片的等,使用 GCD 進行線程間通信非常方便,示例代碼如下:
// 子線程中下載網絡圖片 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 圖片的網絡路徑
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成圖片
UIImage *image = [UIImage imageWithData:data];
// 回到主線程
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
1.7 GCD 其他常用函數
- 阻隔執(zhí)行任務的函數
dispatch_barrier_sync(dispatch_queue_t _Nonnull queue, ^(void)block)
// 此函數起一個阻隔任務執(zhí)行的作用, 它前面的任務執(zhí)行完之后它才執(zhí)行,等它執(zhí)行完后面的任務才能執(zhí)行
- 延遲執(zhí)行
// GCD 延遲執(zhí)行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"run-----");
});
// iOS 中其他方式的延遲執(zhí)行還有
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
和定時器
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:NO];
---------------- run 方法 -----------------
- (void)run
{
NSLog(@"run-----");
}
- 一次性函數
一次性函數在整個程序運行中只會執(zhí)行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"------run-----");
// 內部代碼默認是線程安全的
});
- 快速迭代函數(遍歷)
快速迭代行數,實際上在全局隊列中遍歷子線程執(zhí)行任務,用于顯著提高執(zhí)行效率。
案例:【文件假拷貝】,【App Store 所有App同時更新】讓每個任務都開子線程去并發(fā)執(zhí)行會充分利用CPU,提高效率。
// 本示例代碼是將 From 文件夾下的內容拷貝到 TO 文件夾下
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSString *from = @"/Users/xiaoyou/Desktop/From";
NSString *to = @"/Users/xiaoyou/Desktop/To";
NSFileManager *mgr = [NSFileManager defaultManager];
NSArray *subpaths = [mgr subpathsAtPath:from];
dispatch_apply(subpaths.count, queue, ^(size_t index) {
NSString *subpath = subpaths[index];
NSString *fromFullpath = [from stringByAppendingPathComponent:subpath];
NSString *toFullpath = [to stringByAppendingPathComponent:subpath];
// 剪切
[mgr moveItemAtPath:fromFullpath toPath:toFullpath error:nil];
NSLog(@"%@---%@", [NSThread currentThread], subpath);
});
- GCD 隊列組
隊列組中的任務執(zhí)行完,組會受到一個通知,然后執(zhí)行最終的操作
// 1. 創(chuàng)建全局隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2. 創(chuàng)建一個隊列組
dispatch_group_t group = dispatch_group_create();
// 任務 1.下載圖片1
dispatch_group_async(group, queue, ^{
// 圖片的網絡路徑
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成圖片
self.image1 = [UIImage imageWithData:data];
});
// 任務 2.下載圖片2
dispatch_group_async(group, queue, ^{
// 圖片的網絡路徑
NSURL *url = [NSURL URLWithString:@"http://pic38.nipic.com/20140228/5571398_215900721128_2.jpg"];
// 加載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成圖片
self.image2 = [UIImage imageWithData:data];
});
// 任務 3.將圖片1、圖片2合成一張新的圖片
dispatch_group_notify(group, queue, ^{
// 開啟新的圖形上下文
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
// 繪制圖片
[self.image1 drawInRect:CGRectMake(0, 0, 50, 100)];
[self.image2 drawInRect:CGRectMake(50, 0, 50, 100)];
// 取得上下文中的圖片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 結束上下文
UIGraphicsEndImageContext();
// 回到主線程顯示圖片
dispatch_async(dispatch_get_main_queue(), ^{
// 4.將新圖片顯示出來
self.imageView.image = image;
});
});
2. 使用 GCD 實現單例
2.1 單例模式
單例模式是開發(fā)過程中長期積累的一種編程習慣。
單例模式作用如下:
- 可以保證在程序運行過程中,一個類只有一個實例,而且該實例易于供外界訪問
- 方便控制實例的個數,節(jié)約系統(tǒng)資源
單例模式使用場合:
- 在整個應用中,共享一份資源(該資源只需要創(chuàng)建初始化1次,如Application,NSUserDefault 等)
2.2 單例模式的實現(純代碼)
- 在 .m 中保留一個全局的 static 實例
static id _instance;
- 重寫 allocWithZone: 方法,創(chuàng)建唯一實例
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
// 使用GCD一次性函數,保證線程安全
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [self allocWithZone:zone];
});
return _instance;
}
- 提供類方法,供外界使用
+ (instancetype)shareInstance{
// 使用GCD一次性函數,保證線程安全
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
- 實現 copyWithZone: 方法
+ (id)copyWithZone:(struct _NSZone *)zone
{
return _instance;
}
2.3 單例模式的實現(宏)
從上面的實現中可以看到,單例的實現方式是一樣的,我們可以把它抽取成一個宏來實現,這樣更加方便使用.
如下是單例的宏實現,只需在對應的單例類中添加兩個對應的宏,就可輕松實現單例。
// .h文件
#define XMGSingletonH(name) + (instancetype)shared##name;
// .m文件
#define XMGSingletonM(name) \
static id _instance; \
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [super allocWithZone:zone]; \
}); \
return _instance; \
} \
\
+ (instancetype)shared##name \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [[self alloc] init]; \
}); \
return _instance; \
} \
\
- (id)copyWithZone:(NSZone *)zone \
{ \
return _instance; \
}
思考:為什么不使用繼承?
繼承:看似可行,實際會有問題,程序中的GCD一次性代碼只會執(zhí)行一次,當第一次有子類 A 調用之后,再有子類 B 調用返回的直接是第一次調用 A 的實例,無法返回正確類型 B 單例
也就是說如果有 static 這樣的內部類對象不能用繼承。
3. NSOperation
3.1 NSOperation 簡介
NSOperation 是 OS X 和 iOS 開發(fā)中最后一種多線程實現方式,它是基于 GCD 的 OC 封裝,使用更加面向對象。
- NSOperation 的作用
- 配合使用NSOperation 和 NSOperationQueue 實現多線程
- NSOperation 和 NSOperationQueue 實現多線程的具體步驟
- 先將需要執(zhí)行的操作封裝到一個 NSOperation 對象中
- 然后將 NSOperation 對象添加到 NSOperationQueue 中
- 系統(tǒng)會自動將 NSOperationQueue 中的 NSOperation 取出來,并將封裝的操作放到一條新線程中執(zhí)行
3.2 NSOperation 的子類
NSOperation是個抽象類,并不具備封裝操作的能力,必須使用它的子類
-
使用NSOperation子類的方式有3種
- NSInvocationOperation
- NSBlockOperation
- 自定義子類繼承NSOperation,實現內部相應的方法
NSInvocationOperation
- 創(chuàng)建NSInvocationOperation對象
- (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
- 調用start方法開始執(zhí)行操作
- (void)start;
一旦執(zhí)行操作,就會調用target的sel方法
注意
- 默認情況下,調用了start方法后并不會開一條新線程去執(zhí)行操作,而是在當前線程同步執(zhí)行操作
- 只有將NSOperation放到一個NSOperationQueue中,才會異步執(zhí)行操作
NSBlockOperation
- 創(chuàng)建NSBlockOperation對象
+ (id)blockOperationWithBlock:(void (^)(void))block;
- 通過addExecutionBlock:方法添加更多的操作
- (void)addExecutionBlock:(void (^)(void))block;
注意:
只要NSBlockOperation封裝的操作數 > 1,就會異步執(zhí)行操作
3.3 NSOperationQueue
-
NSOperationQueue的作用
- NSOperation可以調用start方法來執(zhí)行任務,但默認是同步執(zhí)行的
- 如果將NSOperation添加到NSOperationQueue(操作隊列)中,系統(tǒng)會自動異步執(zhí)行NSOperation中的操作
添加操作到NSOperationQueue中
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
3.4 最大并發(fā)數
-
什么是并發(fā)數?
- 同時執(zhí)行的任務數
- 比如,同時開3個線程執(zhí)行3個任務,并發(fā)數就是3
最大并發(fā)數的相關方法
- (NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
3.5 隊列的取消、暫停、恢復
- 取消隊列的所有操作
- (void)cancelAllOperations;
提示:也可以調用NSOperation的- (void)cancel方法取消單個操作
- 暫停和恢復隊列
- (void)setSuspended:(BOOL)b; // YES代表暫停隊列,NO代表恢復隊列
- (BOOL)isSuspended;
3.6 操作依賴
- NSOperation之間可以設置依賴來保證執(zhí)行順序
- 比如一定要讓操作A執(zhí)行完后,才能執(zhí)行操作B,可以這么寫
[operationB addDependency:operationA]; // 操作B依賴于操作A
- 可以在不同queue的NSOperation之間創(chuàng)建依賴關系(如圖)

注意:
不能相互依賴,比如A依賴B,B依賴A
3.7 操作的監(jiān)聽
可以監(jiān)聽一個操作的執(zhí)行完畢
- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;
3.8 自定義NSOperation
自定義NSOperation的步驟很簡單
- 重寫
- (void)main方法,在里面實現想執(zhí)行的任務 - 重寫
- (void)main方法的注意點- 自己創(chuàng)建自動釋放池(因為如果是異步操作,無法訪問主線程的自動釋放池)
- 經常通過
- (BOOL)isCancelled方法檢測操作是否被取消,對取消做出響應
蘋果建議:應該對自定義的 Operation 中的執(zhí)行完一個耗時操作,應該手動調用一下 isCancelled 方法查看是不是已經取消并做對應的操作
/**
* 需要執(zhí)行的任務
*/
- (void)main
{
for (NSInteger i = 0; i<1000; i++) {
NSLog(@"download1 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
for (NSInteger i = 0; i<1000; i++) {
NSLog(@"download2 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
for (NSInteger i = 0; i<1000; i++) {
NSLog(@"download3 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
}
3.9 NSOperation 線程間通信
此處依舊以下載并合成一張圖片為例,只需開啟兩個子線程分別下載image,第三個線程為合并操作, 然后添加線程依賴。并放到隊列中
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block UIImage *image1 = nil;
// 下載圖片1
NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
// 圖片的網絡路徑
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成圖片
image1 = [UIImage imageWithData:data];
}];
__block UIImage *image2 = nil;
// 下載圖片2
NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
// 圖片的網絡路徑
NSURL *url = [NSURL URLWithString:@"http://pic38.nipic.com/20140228/5571398_215900721128_2.jpg"];
// 加載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成圖片
image2 = [UIImage imageWithData:data];
}];
// 合成圖片
NSBlockOperation *combine = [NSBlockOperation blockOperationWithBlock:^{
// 開啟新的圖形上下文
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
// 繪制圖片
[image1 drawInRect:CGRectMake(0, 0, 50, 100)];
image1 = nil;
[image2 drawInRect:CGRectMake(50, 0, 50, 100)];
image2 = nil;
// 取得上下文中的圖片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 結束上下文
UIGraphicsEndImageContext();
// 回到主線程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
[combine addDependency:download1];
[combine addDependency:download2];
[queue addOperation:download1];
[queue addOperation:download2];
[queue addOperation:combine];
簡單的,只有下載圖片然后放到主線程展示的線程通信如下:
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
// 圖片的網絡路徑
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成圖片
UIImage *image = [UIImage imageWithData:data];
// 回到主線程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
4 小結
本文主要講解了 GCD 和 NSOperation 兩種多線程的創(chuàng)建和使用方式。加上上篇文章 共有 pthread 、 NSThread 、 GCD 和 NSOperation 四種多線程方案,實際使用中需要根據項目需求靈活使用。