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可以封装一个任务
- 封装任务
- 把任务添加到队列中
- dispatch_async(queue,任务)
- 会开多条线程,所有的任务是并发执行的
- 注意:使用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_async(queue,任务)
- 会开启一条子线程,所有的任务都是串行执行的,需要等前一个任务执行完,后面的任务才会执行
- 获得队列
-
同步函数+并行队列
- 获得队列
- 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_sync(queue,任务)
- 不会开启新的线程,所有的任务都在当前线程中串行执行
- 获得队列
-
同步函数+串行队列
- 获得队列
- 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_sync(queue,任务)
- 不会开启新的线程,所有的任务都在当前线程中串行执行
- 获得队列
同步函数不管是串行还是并发都是串行执行任务的。
全局并发队列
-
并发队列
- 队列中的任务可以同时执行
- 并发队列分为两种
- 自己创建的并发队列dispatch_queue_create
- 全局并发队列(默认存在)
GCD默认已经提供了全局的并发队列,供整个应用使用,可以无需手动创建
-
dispatch_get_global_queue函数获得全局的并发队列
- 参数一:队列的优先级dispatch_queue_priority
- DISPATCH_QUEUE_PRIORITY_DEFAULT 默认优先级 == 0 ,不要传其他的,会引发线程安全问题(反转)
- 参数二:暂时无用,用0即可,给未来使用的
- 参数一:队列的优先级dispatch_queue_priority
注意:线程的数量并不是由任务的数量决定的(是由系统决定的)
主队列
- 串行队列
- 必须一个接着一个的执行,要等当前任务执行完毕之后,才能执行后面的任务
- 串行队列有两种
- 自己创建的串行队列dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIAL)
- DISPATCH_QUEUE_SERIAL == NULL
- 主队列(和主线程相关)
- 主队列是GCD自带的一种特殊的串行队列
- 放在主队列中的任务,都会放到主线程中执行 | 调度的时候比较特殊!
- dispatch_get_main_queue()获得主队列
- 自己创建的串行队列dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIAL)
- 特点:
- 凡是放在主队列中的任务都必须要在主线程中执行
- 本身是一个串行队列,串行队列的特点它都有
- 如果主队列中有任务需要执行,那么主队列会安排主线程来执行当前队列中的任务,但是在调度之前,会先检查主线程的状态,如果主线程空闲,如果主线程在忙,就暂停调度队列中的任务,等到主线程空闲的时候再调度。
- 主队列+异步函数
- 获得主队列
- 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;
})
- dispatch_async(queue,block)
- 配置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(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;
})
})
- dispatch_group_notify(group,queue,^{
- 应用场景:下载图片1,图片2,把两张图片合成为一张图片
补充
-
GCD的其他用法
- 使用函数的方法封装任务dispatch_async_f()
- 第一个参数:队列
- 第二个参数:函数要接收的参数NULL
- 第三个参数:dispatch_function_t
- void(*dispatch_function_t)(void *)
- (*dispatch_function_t) -- run把第一个小括号替换成函数名称
- 第三个参数直接传run就可以了
- 使用函数的方法封装任务dispatch_async_f()
-
全局并发队列与自己创建的并发队列的区别
- 全局并发队列在整个应用程序中本身是默认存在的,并且对应有高优先级、默认优先级、低优先级和后台优先级一共四个并发队列,我们只选择其中的一个直接拿来用,而creat函数是实打实的从头开始去创建一个队列
- 在ios6.0之前,在GCD中凡是使用了create和retain的函数在最后都需要做一次release操作。而主队列和全局并发队列不需要我们手动release.当然了,在ios6.0之后GCD已经被纳入到了ARC的内存管理范畴中,即便是使用retain或者create函数创建的对象也不再需要开发人员手动释放,我们像对待普通OC对象一样对待GCD就OK
- 在使用栅栏函数的时候,苹果官方明确规定,栅栏函数只有在和使用create函数自己创建的并发队列一起使用的时候才有效果(没有给出具体原因)
- 其他区别涉及到XNU内核的系统级线程编程,不一一列举
- 参考
- GCDAPI
- Libdispatch版本源码