一、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以及多线程技术比较