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

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

上篇文章中介紹了 pthread 和 NSThread 兩種多線程的方式,本文將繼續介紹 GCD 和 NSOperation 這兩種方式。。css

1.GCD

1.1 什麼是GCD

  • GCD 全稱 Grand Central Dispatch,可譯爲「牛逼的中樞調度器」
  • GCD 基於純 C 語言,內部封裝了強大的函數庫

1.2 使用 GCD 有什麼優點

  • GCD 是蘋果公司爲多核並行運算提出的解決方案
  • GCD 會自動利用更多的CPU內核 (如 二核 ,四核)
  • GCD 會自動管理線程的生命週期(建立 、 調度 、 銷燬線程)
  • 程序員只須要告訴 GCD 想要執行什麼任務,不須要編寫任何線程管理代碼

1.3 GCD 的使用

  • GCD 有兩個核心的概念html

    • 任務 : 須要執行的操做
    • 隊列 : 用來存聽任務
  • GCD 的使用步驟nginx

    • 制定任務
    • 將任務放入到隊列中,GCD會自動將隊列中的任務取出,放到對應的線程中執行,隊列中的任務取出遵循 FIFO原則。(FIFO:先進先出,隊列原則)
  • GCD 中有兩個用來執行任務的經常使用函數程序員

    • 同步方法執行任務web

      dispatch_sync(dispatch_queue_t  _Nonnull queue, ^(void)block)
      
          queue : 隊列
          Block : 任務
    • 異步方法執行任務編程

      dispatch_async(dispatch_queue_t  _Nonnull queue, ^(void)block)
  • 同步和異步的區別canvas

    • 同步 : 只能在當前的線程中執行任務,不具有開啓新線程的能力
    • 異步 : 能夠在新的線程中執行任務,具有開啓新線程的能力

1.4 隊列的類型

GCD 的隊列能夠分爲 2 大類安全

  • 併發隊列 ( Concurrent Dispatch Queue )
    • 可讓多任務併發執行,自動開啓多個線程同時執行任務
    • 併發功能只有在異步(dispatch_async)函數下才有效
  • 串行隊列 ( Serial Dispatch Queue )
    • 讓任務一個接一個地有序執行(一個任務執行完畢後纔開始執行下一個)

注意:同步 、 異步、併發、串行的區分ruby

  • 同步異步 主要影響: 能不能開啓新的線程
    • 同步 : 只是在當前線程中執行任務 ,不具有開啓新線程的能力
    • 異步 : 能夠在新的線程中執行任務,具有開啓新縣城的能力
  • 併發串行 主要影響: 任務的執行方式
    • 併發 : 多個任務併發執行
    • 串行 : 多個任務一次順序執行

1.5 GCD 的各類隊列的組合

  • 異步函數 + 併發隊列:能夠同時開啓多條線程
// 1.得到全局的併發隊列
    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]);
        }
    });
  • 同步函數 + 併發隊列:不會開啓新的線程
// 1.得到全局的併發隊列
    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]);
    });
  • 異步函數 + 串行隊列:會開啓新的線程,可是任務是串行的,執行完一個任務,再執行下一個任務
// 1.建立串行隊列
    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]);
    });
  • 異步函數 + 主隊列:只在主線程中執行任務
// 1.建立串行隊列
    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]);
    });

各類隊列的執行效果 :markdown

Snip20170620_1.png

注意:
使用 sync 函數往當前串行隊列中添加任務,會卡住當前的串行隊列

1.6 GCD 個線程之間通訊

一般開闢子線程是爲了執行耗時操做。以下載圖片的等,使用 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 其餘經常使用函數

  • 1. 阻隔執行任務的函數
dispatch_barrier_sync(dispatch_queue_t  _Nonnull queue, ^(void)block)

// 此函數起一個阻隔任務執行的做用, 它前面的任務執行完以後它才執行,等它執行完後面的任務才能執行
  • 2. 延遲執行
// GCD 延遲執行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"run-----");
    });
    
// iOS 中其餘方式的延遲執行還有 
[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-----");
}
  • 3. 一次性函數
一次性函數在整個程序運行中只會執行一次

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
   NSLog(@"------run-----");
   // 內部代碼默認是線程安全的
});
  • 4. 快速迭代函數(遍歷)
快速迭代行數,實際上在全局隊列中遍歷子線程執行任務,用於顯著提升執行效率。
案例:【文件假拷貝】,【App Store 全部App同時更新】讓每一個任務都開子線程去併發執行會充分利用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);
});
  • 5. GCD 隊列組
隊列組中的任務執行完,組會受到一個通知,而後執行最終的操做

// 1. 建立全局隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2. 建立一個隊列組
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.將圖片一、圖片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 單例模式

單例模式是開發過程當中長期積累的一種編程習慣。

單例模式做用以下:

  • 能夠保證在程序運行過程當中,一個類只有一個實例,並且該實例易於供外界訪問
  • 方便控制實例的個數,節約系統資源

單例模式使用場合:

  • 在整個應用中,共享一份資源(該資源只須要建立初始化1次,如Application,NSUserDefault 等)

2.2 單例模式的實現(純代碼)

  • 1. 在 .m 中保留一個全局的 static 實例
static id _instance;
  • 2. 重寫 allocWithZone: 方法,建立惟一實例
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    // 使用GCD一次性函數,保證線程安全
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [self allocWithZone:zone];
    });
    
    return _instance;
}
  • 3. 提供類方法,供外界使用
+ (instancetype)shareInstance{
    // 使用GCD一次性函數,保證線程安全
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    
    return _instance;
}
  • 4. 實現 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一次性代碼只會執行一次,當第一次有子類 A 調用以後,再有子類 B 調用返回的直接是第一次調用 A 的實例,沒法返回正確類型 B 單例

也就是說若是有 static 這樣的內部類對象不能用繼承。

3. NSOperation

3.1 NSOperation 簡介

NSOperation 是 OS X 和 iOS 開發中最後一種多線程實現方式,它是基於 GCD 的 OC 封裝,使用更加面向對象。

  • NSOperation 的做用
    • 配合使用NSOperation 和 NSOperationQueue 實現多線程
  • NSOperation 和 NSOperationQueue 實現多線程的具體步驟
    • 先將須要執行的操做封裝到一個 NSOperation 對象中
    • 而後將 NSOperation 對象添加到 NSOperationQueue 中
    • 系統會自動將 NSOperationQueue 中的 NSOperation 取出來,並將封裝的操做放到一條新線程中執行

3.2 NSOperation 的子類

  • NSOperation是個抽象類,並不具有封裝操做的能力,必須使用它的子類

  • 使用NSOperation子類的方式有3種

    • NSInvocationOperation
    • NSBlockOperation
    • 自定義子類繼承NSOperation,實現內部相應的方法

NSInvocationOperation

  • 建立NSInvocationOperation對象
- (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
  • 調用start方法開始執行操做
- (void)start;

一旦執行操做,就會調用target的sel方法

注意

  • 默認狀況下,調用了start方法後並不會開一條新線程去執行操做,而是在當前線程同步執行操做
  • 只有將NSOperation放到一個NSOperationQueue中,纔會異步執行操做

NSBlockOperation

  • 建立NSBlockOperation對象
+ (id)blockOperationWithBlock:(void (^)(void))block;
  • 經過addExecutionBlock:方法添加更多的操做
- (void)addExecutionBlock:(void (^)(void))block;

注意:

只要NSBlockOperation封裝的操做數 > 1,就會異步執行操做

3.3 NSOperationQueue

  • NSOperationQueue的做用

    • NSOperation能夠調用start方法來執行任務,但默認是同步執行的
    • 若是將NSOperation添加到NSOperationQueue(操做隊列)中,系統會自動異步執行NSOperation中的操做
  • 添加操做到NSOperationQueue中

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

3.4 最大併發數

  • 什麼是併發數?

    • 同時執行的任務數
    • 好比,同時開3個線程執行3個任務,併發數就是3
  • 最大併發數的相關方法

- (NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;

3.5 隊列的取消、暫停、恢復

  • 取消隊列的全部操做
- (void)cancelAllOperations;

提示:也能夠調用NSOperation的- (void)cancel方法取消單個操做

  • 暫停和恢復隊列
- (void)setSuspended:(BOOL)b; // YES表明暫停隊列,NO表明恢復隊列
- (BOOL)isSuspended;

3.6 操做依賴

  • NSOperation之間能夠設置依賴來保證執行順序
    • 好比必定要讓操做A執行完後,才能執行操做B,能夠這麼寫
[operationB addDependency:operationA]; // 操做B依賴於操做A
  • 能夠在不一樣queue的NSOperation之間建立依賴關係(如圖)

Snip20170621_1.png

注意:
不能相互依賴,好比A依賴B,B依賴A

3.7 操做的監聽

能夠監聽一個操做的執行完畢

- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;

3.8 自定義NSOperation

自定義NSOperation的步驟很簡單

  • 重寫- (void)main方法,在裏面實現想執行的任務
  • 重寫- (void)main方法的注意點
    • 本身建立自動釋放池(由於若是是異步操做,沒法訪問主線程的自動釋放池)
    • 常常經過- (BOOL)isCancelled方法檢測操做是否被取消,對取消作出響應
蘋果建議:應該對自定義的 Operation 中的執行完一個耗時操做,應該手動調用一下 isCancelled 方法查看是否是已經取消並作對應的操做

/**
 * 須要執行的任務
 */
- (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 兩種多線程的建立和使用方式。加上上篇文章 共有 pthread 、 NSThread 、 GCD 和 NSOperation 四種多線程方案,實際使用中須要根據項目需求靈活使用。

相關文章
相關標籤/搜索