OS X 和iOS 中的多線程技術(下)

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í)行效果 :

Snip20170620_1.png

注意:
使用 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)建依賴關系(如圖)
Snip20170621_1.png

注意:
不能相互依賴,比如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 四種多線程方案,實際使用中需要根據項目需求靈活使用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 原文:http://www.cocoachina.com/ios/20170707/19769.html 本文主要...
    冬的天閱讀 2,411評論 0 12
  • Object C中創(chuàng)建線程的方法是什么?如果在主線程中執(zhí)行代碼,方法是什么?如果想延時執(zhí)行代碼、方法又是什么? 1...
    AlanGe閱讀 1,908評論 0 17
  • 目錄 一、基本概念1.多線程2.串行和并行, 并發(fā)3.隊列與任務4.同步與異步5.線程狀態(tài)6.多線程方案 二、GC...
    BohrIsLay閱讀 1,693評論 5 12
  • 又住進了醫(yī)院,病是老毛病,單位、鄰居、朋友見慣不驚,沒有幾個來看望他的,很是落寞。 這次,他有...
    許永杰閱讀 587評論 0 0
  • ——那些文章后面你不知道的事: 曾經寫的故事,已經不知道如何再下筆,曾經結束了的東西,不知道如何能重新開始。 離開...
    南山1閱讀 518評論 0 0

友情鏈接更多精彩內容