美文网首页
iOS多线程系列之三:GCD用法大全

iOS多线程系列之三:GCD用法大全

作者: 十拿九稳啦 | 来源:发表于2019-02-27 23:02 被阅读0次

    一、GCD简介

    GCD(Grand Central Dispatch) 伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。

    GCD会自动利用更多的CPU内核;
    会自动管理线程的生命周期(创建线程,调度任务,销毁线程等);
    程序员只需要告诉 GCD 想要如何执行什么任务,不需要编写任何线程管理代码

    一些专业术语

    dispatch :派遣/调度
        
    queue:队列
        用来存放任务的先进先出(FIFO)的容器
    sync:同步
        只是在当前线程中执行任务,不具备开启新线程的能力
    async:异步
        可以在新的线程中执行任务,具备开启新线程的能力
    concurrent:并发
        多个任务并发(同时)执行
    串行:
        一个任务执行完毕后,再执行下一个任务
    

    二、GCD中的核心概念

    1.任务

    任务就是要在线程中执行的操作。我们需要将要执行的代码用block封装好,然后将任务添加到队列并指定任务的执行方式,等待CPU从队列中取出任务放到对应的线程中执行。

     - queue:队列
     - block:任务
    // 1.用同步的方式执行任务
    dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
    
    // 2.用异步的方式执行任务
    dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
    
    // 3.GCD中还有个用来执行任务的函数
    // 在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行
    dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
    

    2.队列

    队列以先进先出按照执行方式(并发/串行)调度任务在对应的线程上执行;
    队列分为:自定义队列、主队列和全局队列;

    <1>自定义队列
    自定义队列又分为:串行队列和并发队列

    • 串行队列
      串行队列一次只调度一个任务,一个任务完成后再调度下一个任务
    // 1.使用dispatch_queue_create函数创建串行队列
    ////OC
    // 创建串行队列(队列类型传递NULL或者DISPATCH_QUEUE_SERIAL)
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", NULL);
    ////Swift
    let serialQueue = DispatchQueue(label: "serialQueue")
    
    // 2.获得主队列
    ////OC
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    ////Swift
    let mainQueue = DispatchQueue.main
    注意:主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行。
    
    • 并发队列
      并发队列可以同时调度多个任务,调度任务的方式,取决于执行任务的函数;并发功能只有在异步的(dispatch_async)函数下才有效;异步状态下,开启的线程上限由GCD底层决定。
    // 1.使用dispatch_queue_create函数创建队列
    dispatch_queue_t
    dispatch_queue_create(const char *label, // 队列名称,该名称可以协助开发调试以及崩溃分析报告 
    dispatch_queue_attr_t attr); // 队列的类型
    
    // 2.创建并发队列
    ////OC
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    ////Swift
    let concurrentQueue = DispatchQueue(label: "concurrentQueue",attributes:.concurrent)
    

    自定义队列在MRC开发时需要使用dispatch_release释放队列

    #if !__has_feature(objc_arc)
        dispatch_release(queue);
    #endif
    

    <2>主队列
    主队列负责在主线程上调度任务,如果在主线程上有任务执行,会等待主线程空闲后再调度任务执行。
    主队列用于UI以及触摸事件等的操作,我们在进行线程间通信,通常是返回主线程更新UI的时候使用到

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 耗时操作
        // ...
        //放回主线程的函数
        dispatch_async(dispatch_get_main_queue(), ^{
            // 在主线程更新 UI
        });
    });
    

    <3>全局并发队列

    全局并发队列是由苹果API提供的,方便程序员使用多线程。

    //使用dispatch_get_global_queue函数获得全局的并发队列
    dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority, unsigned long flags);
    // dispatch_queue_priority_t priority(队列的优先级 )
    // unsigned long flags( 此参数暂时无用,用0即可 )
    
    //获得全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    全局并发队列有优先级

    //全局并发队列的优先级
    #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 // 后台优先级
    

    然而,iOS8 开始使用 QOS(quality of service
    ) 替代了原有的优先级。获取全局并发队列时,直接传递 0,可以实现 iOS 7 & iOS 8 later 的适配。

    //像这样
    dispatch_get_global_queue(0, 0);
    

    <4>全局并发队列与并发队列的区别

    全局并发队列与并发队列的调度方法相同
    全局并发队列没有队列名称
    在MRC开发中,全局并发队列不需要手动释放

    <5>QOS (服务质量) iOS 8.0 推出

    QOS_CLASS_USER_INTERACTIVE:用户交互,会要求 CPU 尽可能地调度此任务,耗时操作不应该使用此服务质量
    QOS_CLASS_USER_INITIATED:用户发起,比 QOS_CLASS_USER_INTERACTIVE 的调度级别低,但是比默认级别高;耗时操作同样不应该使用此服务质量;如果用户希望任务尽快执行完毕返回结果,可以选择此服务质量;
    QOS_CLASS_DEFAULT:默认,此 QOS 不是为添加任务准备的,主要用于传送或恢复由系统提供的 QOS 数值时使用
    QOS_CLASS_UTILITY:实用,耗时操作可以使用此服务质量;
    QOS_CLASS_BACKGROUND:后台,指定任务以最节能的方式运行
    QOS_CLASS_UNSPECIFIED:没有指定 QOS
    

    3.执行任务的函数

    <1>同步(dispatch_sync)

    执行完这一句代码,再执行后续的代码就是同步

    任务被添加到队列后,会当前线程被调度;队列中的任务同步执行完成后,才会调度后续任务。-在主线程中,向主队列添加同步任务,会造成死锁
    -在其他线程中,向主队列向主队列添加同步任务,则会在主线程中同步执行。
    具体是否会造成死锁,以及死锁的原因,还需要针对具体的情况分析,理解队列和执行任务的函数才是关键。实际开发中一般只要记住常用的组合就可以了。
    我们可以利用同步的机制,建立任务之间的依赖关系
    例如:

    用户登录后,才能够并发下载多部小说
    只有“用户登录”任务执行完成之后,多个下载小说的任务才能够“异步”执行
    所有下载任务都依赖“用户登录”

    <2>异步(dispatch_async)

    不必等待这一句代码执行完,就执行下一句代码就是异步

    异步是多线程的代名词,当任务被添加到主队列后,会等待主线程空闲时才会调度该任务;添加到其他线程时,会开启新的线程调度任务。
    <3>以函数指针的方式调度任务
    函数指针的调用方式有两种,同样是同步和异步;函数指针的传递类似于 pthread。

    dispatch_sync_f
    dispatch_async_f
    

    函数指针调用在实际开发中几乎不用,只是有些面试中会问到,dispatch + block 才是 gcd 的主流!

    4.开发中如何选择队列

    选择队列当然是要先了解队列的特点
    串行队列:对执行效率要求不高,对执行顺序要求高,性能消耗小
    并发队列:对执行效率要求高,对执行顺序要求不高,性能消耗大
    如果不想兼顾 MRC 中队列的释放,建议选择使用全局队列 + 异步任务。

    三、GCD的其他用法

    1.延时执行

    参数1:从现在开始经过多少纳秒,参数2:调度任务的队列,参数3:异步执行的任务
    dispatch_after(when, queue, block)
    例如:

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2秒后异步执行这里的代码...
    });
    

    2.一次性执行

    应用场景:保证某段代码在程序运行过程中只被执行一次,在单例设计模式中被广泛使用。

    // 使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行1次的代码(这里面默认是线程安全的)
    });
    

    3.调度组(队列组)

    应用场景:需要在多个耗时操作执行完毕之后,再统一做后续处理

    //创建调度组
    dispatch_group_t group = dispatch_group_create();
    //将调度组添加到队列,执行 block 任务
    dispatch_group_async(group, queue, block);
    //当调度组中的所有任务执行结束后,获得通知,统一做后续操作
    dispatch_group_notify(group, dispatch_get_main_queue(), block);
    

    例如:

    // 分别异步执行2个耗时的操作、2个异步操作都执行完毕后,再回到主线程执行操作
    dispatch_group_t group =  dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 执行1个耗时的异步操作
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 执行1个耗时的异步操作
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步操作都执行完毕后,回到主线程...
    });
    

    4.定时器

    //创建代码
    dispatch_source_t CreateDispatchTimer(uint64_t interval,
      uint64_t leeway,
      dispatch_queue_t queue,
      dispatch_block_t block)
    {
     dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
           0, 0, queue);
     if (timer)
     {
     dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
     dispatch_source_set_event_handler(timer, block);
     dispatch_resume(timer);
     }
     return timer;
    }
    
    • Dispatch Source Timer 是间隔定时器,也就是说每隔一段时间间隔定时器就会触发。在 NSTimer 中要做到同样的效果需要手动把 repeats 设置为 YES。

    • dispatch_source_set_timer 中第二个参数,当我们使用dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时。

    dispatch_time与dispatch_walltime 区别
    使用第一个函数创建的是一个相对的时间,第一个参数开始时间参考的是当前系统的时钟,当 device 进入休眠之后,系统的时钟也会进入休眠状态, 第一个函数同样被挂起; 假如 device 在第一个函数开始执行后10分钟进入了休眠状态,那么这个函数同时也会停止执行,当你再次唤醒 device 之后,该函数同时被唤醒,但是事件的触发就变成了从唤醒 device 的时刻开始,1小时之后
    而第二个函数则不同,他创建的是一个绝对的时间点,一旦创建就表示从这个时间点开始,1小时之后触发事件,假如 device 休眠了10分钟,当再次唤醒 device 的时候,计算时间间隔的时间起点还是 开始时就设置的那个时间点, 而不会受到 device 是否进入休眠影响

    • dispatch_source_set_timer 的第四个参数 leeway 指的是一个期望的容忍时间,将它设置为 1 秒,意味着系统有可能在定时器时间到达的前 1 秒或者后 1 秒才真正触发定时器。在调用时推荐设置一个合理的 leeway 值。需要注意,就算指定 leeway 值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。

    • event handler block 中的代码会在指定的 queue 中执行。当 queue 是后台线程的时候,dispatch timer 相比 NSTimer 就好操作一些了。因为 NSTimer 是需要 Runloop 支持的,如果要在后台 dispatch queue 中使用,则需要手动添加 Runloop。使用 dispatch timer 就简单很多了。

    • dispatch_source_set_event_handler 这个函数在执行完之后,block 会立马执行一遍,后面隔一定时间间隔再执行一次。而 NSTimer 第一次执行是到计时器触发之后。这也是和 NSTimer 之间的一个显著区别。
      停止 Timer

    停止 Dispatch Timer 有两种方法,一种是使用 dispatch_suspend,另外一种是使用 dispatch_source_cancel。

    dispatch_suspend 严格上只是把 Timer 暂时挂起,它和 dispatch_resume 是一个平衡调用,两者分别会减少和增加 dispatch 对象的挂起计数。当这个计数大于 0 的时候,Timer 就会执行。在挂起期间,产生的事件会积累起来,等到 resume 的时候会融合为一个事件发送。
    注意
    dispatch_source_cancel 则是真正意义上的取消 Timer。被取消之后如果想再次执行 Timer,只能重新创建新的 Timer。这个过程类似于对 NSTimer 执行 invalidate。

    关于取消 Timer,另外一个很重要的注意事项:dispatch_suspend 之后的 Timer,是不能被释放的!因此使用 dispatch_suspend 时,Timer 本身的实例需要一直保持。使用 dispatch_source_cancel 则没有这个限制。

    下面的代码会引起崩溃:
    - (void)stopTimer
    {
     dispatch_suspend(_timer);//EXC_BAD_INSTRUCTION 崩溃
     //dispatch_source_cancel(_timer);//OK
     _timer = nil; // 
    }
    

    四、基于GCD的单例模式

    作用:
    可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问。从而方便地控制了实例个数,并节约系统资源
    使用场合:
    在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次)

    实现方法
    重写实现

    // 1.在.m中保留一个全局的static的实例
    static id _instance;
    
    // 2.重写allocWithZone:方法,在这里创建唯一的实例(注意线程安全)
    + (instancetype)allocWithZone:(struct _NSZone *)zone
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _instance = [super allocWithZone:zone];
        });
        return _instance;
    }
    
    // 3.提供1个类方法让外界访问唯一的实例
    + (instancetype)sharedInstance
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _instance = [[self alloc] init];
        });
        return _instance;
    }
    
    // 4.实现copyWithZone:方法
    - (id)copyWithZone:(struct _NSZone *)zone
    {
        return _instance;
    }
    

    宏实现

    // .h文件
    #define SingletonH(name) + (instancetype)shared##name;
    
    // .m文件
    #define SingletonM(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任务

    有一部分人说GCD无法取消任务,也有人站出反对说话不负责任。那么我们先来看看他提供的方案:return就可以正常结束一段代码

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        [self gcdTest];
    }
    
    
    - (void)gcdTest{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            // 模拟耗时操作
            for (long i=0; i<100000; i++) {
                NSLog(@"i:%ld",i);
                sleep(1);
                // 山不过来,我就过去
                if (gcdFlag==YES) {
                    NSLog(@"收到gcd停止信号");
                    return ;
                }
            };
        });
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"发出停止gcd信号!");
            gcdFlag = YES;
        });
    }
    

    GCD中的定时器

    //0.创建一个队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
        //1.创建一个GCD的定时器
        /*
         第一个参数:说明这是一个定时器
         第四个参数:GCD的回调任务添加到那个队列中执行,如果是主队列则在主线程执行
         */
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
        //2.设置定时器的开始时间,间隔时间以及精准度
    
        //设置开始时间,三秒钟之后调用
        dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
        //设置定时器工作的间隔时间
        uint64_t intevel = 1.0 * NSEC_PER_SEC;
    
        /*
         第一个参数:要给哪个定时器设置
         第二个参数:定时器的开始时间DISPATCH_TIME_NOW表示从当前开始
         第三个参数:定时器调用方法的间隔时间
         第四个参数:定时器的精准度,如果传0则表示采用最精准的方式计算,如果传大于0的数值,则表示该定时切换i可以接收该值范围内的误差,通常传0
         该参数的意义:可以适当的提高程序的性能
         注意点:GCD定时器中的时间以纳秒为单位(面试)
         */
    
        dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);
    
        //3.设置定时器开启后回调的方法
        /*
         第一个参数:要给哪个定时器设置
         第二个参数:回调block
         */
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"------%@",[NSThread currentThread]);
        });
    
        //4.执行定时器
        dispatch_resume(timer);
    
        //注意:dispatch_source_t本质上是OC类,在这里是个局部变量,需要强引用
        self.timer = timer;
    
    GCD定时器补充
    /*
     DISPATCH_SOURCE_TYPE_TIMER         定时响应(定时器事件)
     DISPATCH_SOURCE_TYPE_SIGNAL        接收到UNIX信号时响应
    
     DISPATCH_SOURCE_TYPE_READ          IO操作,如对文件的操作、socket操作的读响应
     DISPATCH_SOURCE_TYPE_WRITE         IO操作,如对文件的操作、socket操作的写响应
     DISPATCH_SOURCE_TYPE_VNODE         文件状态监听,文件被删除、移动、重命名
     DISPATCH_SOURCE_TYPE_PROC          进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
    
     下面两个都属于Mach相关事件响应
        DISPATCH_SOURCE_TYPE_MACH_SEND
        DISPATCH_SOURCE_TYPE_MACH_RECV
     下面两个都属于自定义的事件,并且也是有自己来触发
        DISPATCH_SOURCE_TYPE_DATA_ADD
        DISPATCH_SOURCE_TYPE_DATA_OR
     */
    

    iOS多线程系列之一:多线程基础
    iOS多线程系列之二: NSThread
    iOS多线程系列之三:GCD
    iOS多线程系列之四:NSOperation以及多线程技术比较

    相关文章

      网友评论

          本文标题:iOS多线程系列之三:GCD用法大全

          本文链接:https://www.haomeiwen.com/subject/zstcuqtx.html