OS X 和iOS 中的多线程技术(下)

OS X 和iOS 中的多线程技术(下)

上篇文章中介绍了 pthread 和 NSThread 两种多线程的方式,本文将继续介绍 GCD 和 NSOperation 这两种方式。。

1.GCD

1.1 什么是GCD

  • GCD 全称 Grand Central Dispatch,可译为“牛逼的中枢调度器”
  • GCD 基于纯 C 语言,内部封装了强大的函数库

1.2 使用 GCD 有什么优势

  • GCD 是苹果公司为多核并行运算提出的解决方案
  • GCD 会自动利用更多的CPU内核 (如 二核 ,四核)
  • GCD 会自动管理线程的生命周期(创建 、 调度 、 销毁线程)
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

1.3 GCD 的使用

  • GCD 有两个核心的概念

    • 任务 : 需要执行的操作
    • 队列 : 用来存放任务
  • GCD 的使用步骤

    • 制定任务
    • 将任务放入到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行,队列中的任务取出遵循 FIFO原则。(FIFO:先进先出,队列原则)
  • GCD 中有两个用来执行任务的常用函数

    • 同步方法执行任务

          dispatch_sync(dispatch_queue_t  _Nonnull queue, ^(void)block)
      
          queue : 队列
          Block : 任务
      
    • 异步方法执行任务

      dispatch_async(dispatch_queue_t  _Nonnull queue, ^(void)block)
      
  • 同步和异步的区别

    • 同步 : 只能在当前的线程中执行任务,不具备开启新线程的能力
    • 异步 : 可以在新的线程中执行任务,具备开启新线程的能力

1.4 队列的类型

GCD 的队列可以分为 2 大类

  • 并发队列 ( Concurrent Dispatch Queue )
    • 可以让多任务并发执行,自动开启多个线程同时执行任务
    • 并发功能只有在异步(dispatch_async)函数下才有效
  • 串行队列 ( Serial Dispatch Queue )
    • 让任务一个接一个地有序执行(一个任务执行完毕后才开始执行下一个)

注意:同步 、 异步、并发、串行的区分

  • 同步异步 主要影响: 能不能开启新的线程
    • 同步 : 只是在当前线程中执行任务 ,不具备开启新线程的能力
    • 异步 : 可以在新的线程中执行任务,具备开启新县城的能力
  • 并发串行 主要影响: 任务的执行方式
    • 并发 : 多个任务并发执行
    • 串行 : 多个任务一次顺序执行

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]);
    });

各种队列的执行效果 :

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.将图片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 单例模式

单例模式是开发过程中长期积累的一种编程习惯。

单例模式作用如下:

  • 可以保证在程序运行过程中,一个类只有一个实例,而且该实例易于供外界访问
  • 方便控制实例的个数,节约系统资源

单例模式使用场合:

  • 在整个应用中,共享一份资源(该资源只需要创建初始化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 四种多线程方案,实际使用中需要根据项目需求灵活使用。

posted @ 2017-06-21 17:43  xiaoyouPrince  阅读(787)  评论(0编辑  收藏  举报