iOS 多线程(2)- GCD

1.简介

GCD(Grand Central Dispatch ),是Apple开发的一个多核编程的较新的解决方法,纯 C 语言,提供了非常多强大的函数。它主要用于优化应用程序以支持多核处理以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。

1.1 GCD的特点
  • GCD会自动利用更多的CPU内核(比如双核、四核)。
  • GCD自动管理线程的生命周期(创建线程,调度任务,销毁线程等)。
  • 程序员只需要告诉 GCD 想要如何执行什么任务,不需要编写任何线程管理代码。
1.2 GCD的概念
  • 任务(block):任务就是将要在线程中执行的代码,将这段代码用block封装好,然后将这个任务添加到指定的执行方式(同步执行和异步执行),等待CPU从队列中取出任务放到对应的线程中执行。
  • 同步(sync):一个接着一个,前一个没有执行完,后面不能执行,不开线程。
  • 异步(async):开启多个新线程,任务同一时间可以一起执行。异步是多线程的代名词。
  • 队列:装载线程任务的队形结构。(系统以先进先出的方式调度队列中的任务执行)。在GCD中有两种队列:串行队列和并发队列。
  • 并发队列:线程可以同时一起进行执行。实际上是CPU在多条线程之间快速的切换。(并发功能只有在异步(dispatch_async)函数下才有效)。
  • 串行队列:线程只能依次有序的执行。
  • GCD总结:将任务(要在线程中执行的操作block)添加到队列(自己创建或使用全局并发队列),并且指定执行任务的方式(异步dispatch_async,同步dispatch_sync)。
1.3 GCD的创建

1.队列的创建

// 第一个参数const char *label : C语言字符串,用来标识
// 第二个参数dispatch_queue_attr_t attr : 队列的类型
// 并发队列:DISPATCH_QUEUE_CONCURRENT
// 串行队列:DISPATCH_QUEUE_SERIAL 或者 NULL
dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

//创建并发队列
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);
//创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
//GCD默认已经提供了全局并发队列,供整个应用使用,可以无需手动创建
/** 
     第一个参数:优先级 也可直接填后面的数字
     #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
     #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认
     #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
     #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
     第二个参数: 预留参数  0
     */
    dispatch_queue_t quque1 =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//获得主队列
dispatch_queue_t  queue = dispatch_get_main_queue();
  1. 任务的执行
    队列在queue中,任务在block块中
//开启同步函数 同步函数:要求立刻马上开始执行
dispatch_queue_t  queue = dispatch_get_main_queue();
/*
     第一个参数:队列
     第二个参数:block,在里面封装任务
     */
    dispatch_sync(queue, ^{
        
    });
//开启异步函数 异步函数 :等主线程执行完毕之后,回过头开线程执行任务
   dispatch_async(queue, ^{
        
    });

3.任务和队列的组合

  • 任务:同步函数 异步函数
  • 队列:串行 并行
  • 异步函数+并发队列:会开启新的线程,并发执行
 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
 dispatch_async(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
 });
  • 异步函数+串行队列:会开启一条线程,任务串行执行
 dispatch_queue_t queue =  dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
 dispatch_async(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
 });
  • 同步函数+并发队列:不会开线程,任务串行执行
 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
 dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
   });
  • 同步函数+串行队列:不会开线程,任务串行执行
  dispatch_queue_t queue =  dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);   
  dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
});
  • 异步函数+主队列:不会开线程,任务串行执行
    使用主队列(跟主线程相关联的队列)
    主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行
//1.获得主队列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.异步函数
dispatch_async(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });
  • 同步函数+主队列:死锁
//1.获得主队列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.同步函数
dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
});

因为这个方法在主线程中,给主线程中添加任务,而同步函数要求立刻马上执行,因此就会相互等待而发生死锁。将这个方法放入子线程中,则不会发生死锁,任务串行执行。

  • 总结:同步函数和异步函数的执行顺序
    同步函数:立刻马上执行,会阻塞当前线程。
    异步函数:当前线程会直接往下执行,不会阻塞当前线程。
  • 注意:GCD中开多少条线程是由系统根据CUP繁忙程度决定的,如果任务很多,GCD会开启适当的子线程,并不会让所有任务同时执行。
1.4 GCD线程间的通信
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        // 获得图片URL
        NSURL *url = [NSURL URLWithString:@"//upload-images.jianshu.io/upload_images/2301429-d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
        // 将图片URL下载为二进制文件
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 将二进制文件转化为image
        UIImage *image = [UIImage imageWithData:data];
        NSLog(@"%@",[NSThread currentThread]);
        // 返回主线程 这里用同步函数不会发生死锁,因为这个方法在子线程中被调用。
        // 也可以使用异步函数
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@",[NSThread currentThread]);
        });
    }); 
}
@end

通过上面的例子我们可以得出,GCD线程间的通信非常简单,使用同步或异步函数,传入主队列即可。

1.5 GCD其他常用函数
  • 1.dispatch_barrier_async
    当任务需要异步进行,但是这些任务需要分成两组来执行,第一组完成之后才能进行第二组的操作。这时候就用了到GCD的栅栏方法dispatch_barrier_async。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 并发队列
    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    // 异步执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"栅栏:并发异步1   %@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"栅栏:并发异步2   %@",[NSThread currentThread]);
        }
    });

    dispatch_barrier_async(queue, ^{
        NSLog(@"------------barrier------------%@", [NSThread currentThread]);
        NSLog(@"******* 并发异步执行,但是34一定在12后面 *********");
    });

    dispatch_async(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"栅栏:并发异步3   %@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"栅栏:并发异步4   %@",[NSThread currentThread]);
        }
    });
}

上面代码的打印结果如下,开启了多条线程,所有任务都是并发异步进行。但是第一组完成之后,才会进行第二组的操作。

栅栏:并发异步1   {number = 3, name = (null)}
栅栏:并发异步2   {number = 6, name = (null)}
栅栏:并发异步1   {number = 3, name = (null)}
栅栏:并发异步2   {number = 6, name = (null)}
栅栏:并发异步1   {number = 3, name = (null)}
栅栏:并发异步2   {number = 6, name = (null)}
 ------------barrier------------{number = 6, name = (null)}
******* 并发异步执行,但是34一定在12后面 *********
栅栏:并发异步4   {number = 3, name = (null)}
栅栏:并发异步3   {number = 6, name = (null)}
栅栏:并发异步4   {number = 3, name = (null)}
栅栏:并发异步3   {number = 6, name = (null)}
栅栏:并发异步4   {number = 3, name = (null)}
栅栏:并发异步3   {number = 6, name = (null)}
  • 注意栅栏函数只能是自定义的并发队列,全局队列也不行。
    1. GCD延时执行
      当需要等待一会再执行一段代码时,就可以用到这个方法了:dispatch_after。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 5秒后异步执行
    NSLog(@"我已经等待了5秒!");
});
//延迟执行的其他方法:
// 2s中之后调用run方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// repeats:YES 是否重复
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
  • 3.dispatch_once
    使用dispatch_once能保证某段代码在程序运行过程中只被执行1次。可以用来设计单例。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"程序运行过程中我只执行了一次!");
});
  • 4.GCD快速迭代
    GCD有一个快速迭代的方法dispatch_apply,dispatch_apply可以同时遍历多个数字,开启多条线程,并发执行,相比于for循环在耗时操作中极大的提高效率和速度。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    NSLog(@"

************** GCD快速迭代 ***************

");

    // 并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    // dispatch_apply几乎同时遍历多个数字
    dispatch_apply(7, queue, ^(size_t index) {
        NSLog(@"dispatch_apply:%zd======%@",index, [NSThread currentThread]);
    });
}
//打印结果
dispatch_apply:0======{number = 1, name = main}
dispatch_apply:1======{number = 1, name = main}
dispatch_apply:2======{number = 1, name = main}
dispatch_apply:3======{number = 1, name = main}
dispatch_apply:4======{number = 1, name = main}
dispatch_apply:5======{number = 1, name = main}
dispatch_apply:6======{number = 1, name = main}
  • 队列组(同栅栏函数)
    异步执行几个耗时操作,当这几个操作都完成之后再回到主线程进行操作,就可以用到队列组了。
    队列组有下面几个特点:
    1). 所有的任务会并发的执行(不按序)。
    2). 所有的异步函数都添加到队列中,然后再纳入队列组的监听范围。
    3). 使用dispatch_group_notify函数,来监听上面的任务是否完成,如果完成, 就会调用这个方法。
- (void)testGroup {
    dispatch_group_t group =  dispatch_group_create();

    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"队列组:有一个耗时操作完成!");
    });

    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"队列组:有一个耗时操作完成!");
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"队列组:前面的耗时操作都完成了,回到主线程进行相关操作");
    });
}
//打印结果
队列组:有一个耗时操作完成!
队列组:有一个耗时操作完成!
队列组:前面的耗时操作都完成了,回到主线程进行相关操作
    1. 信号量:dispatch_semaphore
      我们在开发中,会遇到这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。比如说:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

下面,我们来利用 Dispatch Semaphore 实现线程同步,将异步执行任务转换为同步执行任务。

/**
 * semaphore 线程同步
 */
- (void)semaphoreSync {
    
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        
        number = 100;
        
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %zd",number);
}
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容