GCD

GCD

GCD简介

  • Grand Central Dispatch中枢调度器
  • 纯C语言的,提供了非常强大的函数
  • 优势
    • GCD是苹果公司为多核的并行运算提出的解决方案
    • GCD会自动利用更多的CPU内核(比如双核、四核),充分利用设备的多核
    • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
    • github上面有源码

任务和队列

  • GCD中的核心概念
  • 任务:执行什么操作,需要用函数封装起来(同步函数|异步函数)
  • 队列:用来存放任务的
  • 使用步骤
    • 定制任务
      • 确定想要做的事情
    • 将任务添加到队列中
      • GCD会自动将队列中的任务取出,放到对应的线程中执行
      • 任务的取出遵循队列的FIFO原则:先进先出,后进后出
  • 执行任务
    • 同步
      • dispatch_sync(dispatch_queue_t queue,dispatch_block_t block)
      • 只能在当前线程中执行任务,不具备开启新线程的能力
    • 异步
      • dispatch_asyn
      • 可以在新的线程中执行任务,具备开启新线程的能力
  • 队列的类型
    • 并发队列
      • 可以让多个任务并发执行
      • 并发功能只有在异步函数下才有效
    • 串行队列
      • 让任务一个接着一个的执行
  • 容易混淆的术语
    • 同步异步:能不能开启新的线程
      • 同步:只是在当前线程中执行任务,不具备开启新线程的能力
      • 异步:可以在新的线程中执行任务,具备开启新线程的能力
    • 并发|串行:任务的执行方式
      • 并发:允许多个任务并发执行
      • 串行:一个任务执行完毕之后,再执行下一个任务

GCD基本使用

  • 四种组合方式

  • 异步函数+并发队列

    • 获得队列
      • dispatch_queue_t 结构体
      • dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_CONCURRENT)创建
        • 第一个参数:C语言的字符串,给队列起一个名字(推荐写法:公司域名倒过来写,后面跟上作用)名字的作用:是用来调试的
        • 第二个参数:队列的类型,传一个宏
          • DISPATCH_QUEUE_CONCURRENT
          • DISPATCH_QUEUE_SERIAL
    • 封装任务并且把任务添加到队列中
      • dispatch_async(queue,任务)
        • 第一个参数:队列
        • 第二个参数:block可以封装一个任务
      • 封装任务
      • 把任务添加到队列中
    • 会开多条线程,所有的任务是并发执行的
    • 注意:使用GCD的时候,具体开几条线程,并不是由任务的数量来决定的,是由系统自动决定
      • 会看CPU的处理情况,线程是可以复用的
  • 异步函数+串行队列

    • 获得队列
      • dispatch_queue_t 结构体
      • dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIAL)创建
        • 第一个参数:C语言的字符串,给队列起一个名字(推荐写法:公司域名倒过来写,后面跟上作用)
        • 第二个参数:队列的类型,传一个宏
          • DISPATCH_QUEUE_CONCURRENT
          • DISPATCH_QUEUE_SERIAL
    • 封装任务并且把任务添加到队列中
      • dispatch_async(queue,任务)
        • 第一个参数:队列
        • 第二个参数:block可以封装一个任务
      • 封装任务
      • 把任务添加到队列中
    • 会开启一条子线程,所有的任务都是串行执行的,需要等前一个任务执行完,后面的任务才会执行
  • 同步函数+并行队列

    • 获得队列
      • dispatch_queue_t 结构体
      • dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_CONCURRENT)创建
        • 第一个参数:C语言的字符串,给队列起一个名字(推荐写法:公司域名倒过来写,后面跟上作用)
        • 第二个参数:队列的类型,传一个宏
          • DISPATCH_QUEUE_CONCURRENT
          • DISPATCH_QUEUE_SERIAL
    • 封装任务并且把任务添加到队列中
      • dispatch_sync(queue,任务)
        • 第一个参数:队列
        • 第二个参数:block可以封装一个任务
      • 封装任务
      • 把任务添加到队列中
    • 不会开启新的线程,所有的任务都在当前线程中串行执行
  • 同步函数+串行队列

    • 获得队列
      • dispatch_queue_t 结构体
      • dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIALRENT)创建
        • 第一个参数:C语言的字符串,给队列起一个名字(推荐写法:公司域名倒过来写,后面跟上作用)
        • 第二个参数:队列的类型,传一个宏
          • DISPATCH_QUEUE_CONCURRENT
          • DISPATCH_QUEUE_SERIAL
    • 封装任务并且把任务添加到队列中
      • dispatch_sync(queue,任务)
        • 第一个参数:队列
        • 第二个参数:block可以封装一个任务
      • 封装任务
      • 把任务添加到队列中
    • 不会开启新的线程,所有的任务都在当前线程中串行执行
  • 同步函数不管是串行还是并发都是串行执行任务的。

全局并发队列

  • 并发队列

    • 队列中的任务可以同时执行
    • 并发队列分为两种
      • 自己创建的并发队列dispatch_queue_create
      • 全局并发队列(默认存在)
  • GCD默认已经提供了全局的并发队列,供整个应用使用,可以无需手动创建

  • dispatch_get_global_queue函数获得全局的并发队列

    • 参数一:队列的优先级dispatch_queue_priority
      • DISPATCH_QUEUE_PRIORITY_DEFAULT 默认优先级 == 0 ,不要传其他的,会引发线程安全问题(反转)
    • 参数二:暂时无用,用0即可,给未来使用的
  • 注意:线程的数量并不是由任务的数量决定的(是由系统决定的)

主队列

  • 串行队列
    • 必须一个接着一个的执行,要等当前任务执行完毕之后,才能执行后面的任务
    • 串行队列有两种
      • 自己创建的串行队列dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIAL)
        • DISPATCH_QUEUE_SERIAL == NULL
      • 主队列(和主线程相关)
        • 主队列是GCD自带的一种特殊的串行队列
        • 放在主队列中的任务,都会放到主线程中执行 | 调度的时候比较特殊!
        • dispatch_get_main_queue()获得主队列
  • 特点:
    • 凡是放在主队列中的任务都必须要在主线程中执行
    • 本身是一个串行队列,串行队列的特点它都有
    • 如果主队列中有任务需要执行,那么主队列会安排主线程来执行当前队列中的任务,但是在调度之前,会先检查主线程的状态,如果主线程空闲,如果主线程在忙,就暂停调度队列中的任务,等到主线程空闲的时候再调度。
  • 主队列+异步函数
    • 获得主队列
      • dispatch_get_main_queue()
    • 添加任务到队列中
      • dispatch_async()
    • 不会开线程,所有的任务都在主线程中串行执行
  • 主队列+同步函数
    • 获得主队列
      • dispatch_get_main_queue()
    • 添加任务到队列中
      • dispatch_sync()
    • 任务并没有执行,有一个死锁
      • 主队列里的任务必须要在主线程中执行
      • 主线程在执行同步函数
      • 主队列工作模式:如果发现队列中有任务,那么就安排主线程来执行,但是在安排之前, 会先检查主线程的状态(看主线程是否空闲),如果主线程在忙,那么就暂停调度,直到主线程空闲
    • 如果在子线程中调度就不会死锁
      • performSelectorInBackground:@selector(主队列+同步函数) withObject:
      • 不会发生死锁了,为什么?
        • 主线程空闲,没有事情做,主线程就执行了任务
  • 开发中,常用的是:异步函数+并发队列|串行队列,面试中会把代码给你,问你打印的结果是什么

  • 问题:同步函数+主队列会死锁?异步函数+主队列不会死锁?

    • 同步异步:
      • 是否具有开线程的能力
      • 同步函数同步执行任务,异步函数异步执行任务
      • 同步执行:
        • 必须要等当前任务执行完,才能执行后面的任务,如果我没有执行,那么我后面的也别想执行
      • 异步执行:
        • 我开始执行后,可以不用等我执行完,就执行后面的任务
      • 异步函数+主队列不会发生死锁
        • 获得一个主队列
        • 同步函数
          • 封装一个任务
          • 把任务添加到队列中去,检查主线程状态(较忙)死锁
        • 异步函数
          • 队列中有任务,要求主线程去执行
          • 异步函数异步执行,跳过任务,把任务存在队列中
          • 代码执行完毕后,主线程空闲了,就回来再执行队列里的任务,就不会发生死锁
  • 异步函数 +并发队列 ps 同步函数+并发队列
    • 1.start - end - 任务 - 异步
    • 2.start - 任务 - end - 同步
  • 问题:同步函数+串行队列不会发生死锁?

    • 串行队列,在当前线程中执行,串行队列有任务就会执行,不会检查主线程的状态, 不管线程是否在忙,都会强行让线程来执行任务
  • 总结

    • 异步函数+不是主队列就一定会开线程
    • 同步+并发:不会线程,串行执行任务
    • 同步+串行:不会开线程,串行执行任务
    • 同步+主队列:不会开线程,死锁
    • 异步+并发:开多条线程,并发执行任务
    • 异步+串行:开线程,串行执行任务
    • 异步+主队列:没有开启新线程,串行执行任务

线程间通信

  • 开子线程下载图片
    • 异步+非主队列
    • 获得全局并发队列
      • dispatch_get_global_queue(0,0)
    • 使用异步函数+并发队列
      • dispatch_async(queue,block)
        • 确定URL
        • 把图片的二进制数据下载到本地
          • dataWithContentsOfURL:
        • 转化格式
          • imageWithData:
        • 显示图片
          • 回到主线程刷新UI
          • GCD可以嵌套
          • dispatch_async(dispatch_get_main_queue(),^{
            self.imageV.image = image;
            })
    • 配置info.plist文件
  • 问:回到主线程,是异步函数+主队列,在这里可以用同步函数+主队列吗?
    • 可以,当前执行的是子线程,不会发生死锁

常用函数

  • 开发中比较常用的GCD函数
延迟执行
  • 一段时间之后再执行任务
  • 三种方法可以实现延迟执行
    • 方法一:performSelector:withObject:afterDelay:
    • 方法二:定时器NSTimer scheduledTimeWithTimeInterval:target:selector:userInfo:repeats:
    • 方法三:GCD dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(3.0*NSEC_PER_SEC)),dispatch_get_main_queue),^{}
      • 默认是主队列 ,可以改为全局并发队列
      • 队列决定代码块在哪个线程中调用
      • 主队列 - 主线程
      • 并发队列 - 子线程
      • 好处:可以控制在哪个线程里面执行任务
  • 延迟执行内部实现原理?
    • A:先把任务提交到队列,然后三秒之后再执行该任务(错误)
    • B:先等三秒的时间,然后再把任务提交到队列
    • 正确答案:B
    • 延迟执行就是延迟提交
一次性代码
  • 只会执行一次,一辈子只会执行一次
  • dispatch_once(&onceToken,^{});
  • 特点:
    • 整个程序运行过程中,只会执行一次
    • 它本身是线程安全的,不用考虑线程安全的问题
  • 怎么做到永远只会执行一次呢?
    • 通过静态变量的值来判断的
      • static dispatch_once_t onceToken;
    • 判断onceToken值是否等于0,不等于零就不执行了
快速迭代
  • 迭代 - 递归 - 遍历 - 博客ios开发中实用技巧

  • 遍历

    • for循环,开多条线程就可以提高效率
    • GCD的快速迭代dispatch_apply(10,并发队列,^(size_t index){})
      • 参数一:遍历多少次,size_t 类似NSInteger类型
      • 参数二:对列,看到队列就要想,block块在哪个线程中调用
      • 参数三:^(size_t ){}类似于for循环里面的i
  • for循环与GCD的快速迭代区别

    • 快速迭代没有顺序,开始迭代的时候是按照顺序开始的,谁先结束是不知道的,所以打印出的结果是没有顺序的,是并发执行的
    • GCD快速迭代:会开启多条子线程和主线程一起并发的执行任务
    • 注意:队列怎么传?不能传主队列!不传串行队列,不会死锁,不会开线程,执行方式和for循环一样,串行执行的!传并发队列!!!
    • 用快速迭代就用并发队列!!!!
  • 快速迭代的应用

    • 剪切文件
      • 获得路径下面的子路径(打印的是文件的名称,剪切的时候需要的是文件的全路径)
        文件管理者NSFileManager defaultManager]subpathsAtPath:
      • 遍历数组(快速迭代)
dispatch_get_global_queue(0,0)
dispatch_apply(subpaths.count,queue,^(size_t){
                获得文件的名称
                获得要剪切文件的全路径(stringByAppendingPathComponent:该方法在拼接的时候会自动添加一个/)
                拼接文件的目标路径(toFullPath)
                剪切文件
                [NSFileManager defaultManager] moveItemAtpath:toPath:error:(第一个参数:要剪切的文件在哪里?第二参数:要剪切到哪里去,全路径;第三个参数:)
                }) 
栅栏函数
  • 获得队列:并发
    • dispatch_queue_create("",DISPATCH_QUEUE_CONCURRENT)
    • 为什么不用全局并发队列?
      • 栅栏函数不能使用全局并发队列
      • 如果使用全局并发队列,栅栏函数就不具备拦截功能,和普通的异步函数没有任何区别
      • 使用栅栏函数,必须要自己创建队列
  • 异步函数
    • dispatch_async(queue,^{})
  • 需求:真实开发中,任务和任务之间有依赖关系,有先后顺序关系,要求执行123三个任务后,执行打印操作,再执行后面的456三个任务
  • 栅栏函数
    • dispatch_barrier_async()一般用异步函数
    • 在栅栏函数内部执行打印操作
    • 作用:
      • 等之前的所有任务都执行完毕之后,执行栅栏函数中的任务,等我的任务执行完毕之后,在执行后面的任务
      • 前面的和后面的任务都是并发执行的
GCD队列组(掌握)
  • 和栅栏函数的功能很类似

  • 1.获得队列

    • 全局并发队列dispatch_get_global_queue()
  • 异步函数dispatch_async(queue,^{})

  • 需求,在最后拦住任务,当所有的任务执行完毕之后做一些事情!

  • 队列组(调度组)

  • 2.创建队列组dispatch_group_create()

    • 监听队列里面所有任务的执行情况
  • 3.异步函数dispatch_group_async()

    • 第一个参数:队列组
    • 第二个参数:队列
    • 通过队列组建立起队列组和队列以及任务之间的关系
  • 4.拦截dispatch_group_notify(group,queue,^{打印操作})当队列组中所有的任务都执行完毕之后,会调用该方法

  • 队列组是如何知道任务什么时候结束的呢?

    • 以前是把dispatch_group_async(group,queue,^{})拆分成三个方法
      • 1.创建队列组
      • 2.获得对列
      • 3.该方法后面的异步任务会被队列组监听dispatch_group_enter(group)
      • 4.使用异步函数封装任务
      • dispatch_async(queue,^{任务1,在里面监听,enter和leave配对使用dispatch_group_leave(group)})
      • 5.拦截通知,队列决定block块在哪个线程中执行 dispatch_group_notify(group,queue,^{})
  • dispatch_group_async()和dispatch_async()的区别

    • 相同点:
      • 封装任务
      • 把任务添加到队列
    • 不同点
      • 队列组能够监听该任务的执行情况(开始|结束)
  • 队列组的应用

    • 应用场景:下载图片1,图片2,把两张图片合成为一张图片
      • 耗时操作,耗时操作放到子线程中执行
      • 要开2~3条子线程
      • 获得一个并发队列dispatch_get_global_queue(0,0)
      • 合成图片操作,有一个隐藏的依赖关系
        • 队列组
        • 栅栏函数
      • 创建队列组dispatch_group_creat()
      • 异步函数dispatch_group_async(group,queue^{
        URL:URLWithString
        把图片的二进制数据下载到本地dataWithContentOfURL:
        转换格式imageWithData
        定义图片的属性,强引用转换好的图片
        })
      • 合成图片
        • dispatch_group_notify(group,queue,^{
          开启图形上下文UIGraphicsBeginImageContext()
          画图1和2 drawInRect:
          根据上下文得到图片 UIGraphicsGetImageFromCurrentImageContext()
          关闭上下文UIGraphicsEndImageContext()
          显示图片(线程间通信)
          dispatch_async(dispatch_get_main_queue(),^{
          self.imageV.image = image;
          })
          })

补充

  • GCD的其他用法

    • 使用函数的方法封装任务dispatch_async_f()
      • 第一个参数:队列
      • 第二个参数:函数要接收的参数NULL
      • 第三个参数:dispatch_function_t
        • void(*dispatch_function_t)(void *)
        • (*dispatch_function_t) -- run把第一个小括号替换成函数名称
        • 第三个参数直接传run就可以了
  • 全局并发队列与自己创建的并发队列的区别

    • 全局并发队列在整个应用程序中本身是默认存在的,并且对应有高优先级、默认优先级、低优先级和后台优先级一共四个并发队列,我们只选择其中的一个直接拿来用,而creat函数是实打实的从头开始去创建一个队列
    • 在ios6.0之前,在GCD中凡是使用了create和retain的函数在最后都需要做一次release操作。而主队列和全局并发队列不需要我们手动release.当然了,在ios6.0之后GCD已经被纳入到了ARC的内存管理范畴中,即便是使用retain或者create函数创建的对象也不再需要开发人员手动释放,我们像对待普通OC对象一样对待GCD就OK
    • 在使用栅栏函数的时候,苹果官方明确规定,栅栏函数只有在和使用create函数自己创建的并发队列一起使用的时候才有效果(没有给出具体原因)
    • 其他区别涉及到XNU内核的系统级线程编程,不一一列举
    • 参考
      • GCDAPI
      • Libdispatch版本源码

你可能感兴趣的:(GCD)