1. GCD 概要
Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。GCD的优势:
- GCD 是苹果公司为多核的并行运算提出的解决方案
- GCD 会自动利用更多的CPU内核(比如双核、四核)
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
2. 队列 dispatch_queue_t
用来存放等待执行任务的队列。GCD 会自动将队列中的任务取出,放到对应的线程中执行。任务的取出遵循队列的 FIFO 原则(先进先出)。在 GCD 中有两种队列: 串行队列 和 并发队列。两者都符合 FIFO的原则。两者的主要区别是:
-
串行队列: 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)多个串行队列可以并发执行。
-
并发队列: 可以让多个任务并发执行。可以开启多个线程同时执行任务,并发队列的并发功能只有在异步方法下才有效。
2.1 创建队列
第一种方法是通过dispatch_queue_create函数生成队列。以下源代码生成串行队列:
//第一个参数 队列的名称,(C语言字符串)队列的唯一标识符,用于DEBUG,可为空。推荐使用应用程序ID逆序全程域名。
//第二个参数用来识别是串行队列还是并发队列。
//DISPATCH_QUEUE_SERIAL 表示串行队列,也可以 用 NULL
//DISPATCH_QUEUE_CONCURRENT 表示并发队列。
// dispatch_queue_create 返回值为 dispatch_queue_t 类型
// 串行队列的创建方法
dispatch_queue_t serialQueue = dispatch_queue_create("SerialQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
关于串行队列生成个数的注意事项。
如前所述,并发队列并行执行多个任务,而串行队列同时只能执行 1 个任务。虽然串行队列和并发队列受到系统资源的限制,但用dispatch_queue_create函数可生成任意多个队列。
当生成多个串行队列时,各个串行队列可以并行执行。虽然在 1 个串行队列中同时只能执行 1 个任务,但如果将任务分别追加到多个串行队列中,各个串行队列执行 1 个,即为同时执行多个任务。
以上是关于串行队列生成个数注意事项的说明。一旦生成串行队列并追加处理,系统对于一个串行队列就只生成并使用一个线程。如果生成 2000 个串行队列,那么就生成 2000 个线程。如果过多使用多线程,就会消耗大量内存,大幅度降低系统的响应性能。
只在为了避免多线程编程问题之一多个线程更新相同资源导致数据竞争时使用串行队列。
当想并行执行不发生数据竞争等问题的处理时,使用并发队列。而且对于并发队列说,不管生成多少,由于 XNU 内核只使用有效管理的线程,因此不会发生串行队列的那些问题。
2.2 主队列/全局队列
第二种方法是获取系统标准提供的队列,主队列和全局队列。
主队列是在主线程中执行的队列,因为主线程只有 1 个,所以主队列自然就是串行队列。
追加到主队列的任务在主线程的 RunLoop 中执行。由于在主线程中执行,因此要将用户界面的界面更新等一些必须在主线程中执行的处理追加到主队列使用。
全局队列是所有应用程序都能够使用的并发队列。
全局队列有 4 个执行优先级,分别是高优先级(High Priority)、默认优先级( Default Priority)、低优先级(Low Priority)和后台优先级(Background Priority)。通过 XNU 内核管理的用于全局队列的线程,将各自使用的全局队列的执行优先级作为线程的执行优先级使用。在向全局队列追加处理时,应选择与处理内容对应的执行优先级的全局队列。
但是通过 XNU 内核用于全局队列的线程并不能保证实时性,因此执行优先级只是大致的判断。
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();
//主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,有都会放到主线程中去执行,所以才造成了主队列特殊的现象。
// 全局并发队列的获取方法
//第一个参数 优先级
// DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
// DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)
// DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
// DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
//第二个参数 为将来使用而保留的标志。始终为此参数指定0。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
创建的并发队列和全局并发队列的区别:
- 全局并发队列在整个应用程序中本身是默认存在的并且对应有高优先级、默认优先级、低优先级和后台优先级一共四个并发队列,我们只是选择其中的一个直接拿来用。而Create函数是从头开始去创建一个队列。
- 在 iOS6.0 之前,在 GCD 中凡是使用了带 Create 和 retain 的函数在最后都需要做一次release操作。而主队列和全局并发队列不需要我们手动release。当然了,在 iOS6.0之后GCD已经被纳入到了ARC的内存管理范畴中,即便是使用retain或者create函数创建的对象也不再需要开发人员手动释放,我们像对待普通 OC 对象一样对待GCD。
- 在使用栅栏函数的时候,苹果官方明确规定栅栏函数只有在和使用create函数自己的创建的并发队列一起使用的时候才有效(没有给出具体原因)。
- 其它区别涉及到XNU内核的系统级线程编程。
3. 任务 dispatch_block_t
GCD中有 2 个用来执行任务的常用函数
同步执行(sync)
//dispatch_queue_t queue 队列
//dispatch_block_t block 任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async)
//dispatch_queue_t queue 队列
//dispatch_block_t block 任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
可以在新的线程中执行任务,具备开启新线程的能力。
注意:异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关
同步和异步区别
- 同步:只能在当前线程中执行任务,不具备开启新线程的能力,必须等待之前任务执行结束,才能执行下一个任务。
- 异步:可以在新的线程中执行任务,具备开启新线程的能力,不必等待之前任务执行结束,就能执行下一个任务。
3.1 创建任务
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});
3.2 队列与任务不同组合
两种队列(串行队列 / 并发队列),两种任务执行方式(同步执行 / 异步执行),四种不同的组合方式。这四种不同的组合方式是:
- 同步执行 + 并发队列
- 异步执行 + 并发队列
- 同步执行 + 串行队列
- 异步执行 + 串行队列
还有两种默认队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用。这样就有六种不同的组合方式。
- 同步执行 + 主队列
- 异步执行 + 主队列
主线程下(暂时不考虑队列中嵌套队列),不同队列 +不同任务 简单组合的区别:
| 区别 | 并发队列 | 串行队列 | 主队列 |
|---|---|---|---|
| 同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 死锁卡住不执行 |
| 异步(async) | 有开启新线程,并发执行任务 | 有开启新线程(1条),串行执行任务 | 没有开启新线程,串行执行任务 |
注意:从上边可看出: 主线程中调用 主队列 +同步执行 会导致死锁问题。 这是因为 主队列中追加的同步任务 和 主线程本身的任务 两者之间相互等待,阻塞了 主队列,最终造成了主队列所在的线程(主线程)死锁问题。 而如果我们在 其他线程调用 主队列 +同步执行,则不会阻塞 主队列,自然也不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务。
4. GCD 基本使用
GCD 的使用步骤:
- 创建一个队列(串行队列或并发队列);
- 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)。
4.1 异步执行 + 并发队列
每个队列会开启多条子线程,所有的任务并发执行. 开几条线程并不是由任务的数量决定,由 GCD 内部自动决定。
//01 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"----start----");
//02 封装任务,把任务添加到队列
dispatch_async(queue, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_async(queue, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
dispatch_async(queue, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
NSLog(@"-----end-----");
4.2 异步函数 + 串行队列
- 每个队列会开启一条子线程,同一队列所有的任务在该子线程中串行执行,多个队列并发执行。
//01 创建串行队列
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_SERIAL);
//02 封装任务,把任务添加到队列
NSLog(@"----start----");
//02 封装任务,把任务添加到队列
dispatch_async(queue1, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_async(queue1, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
dispatch_async(queue2, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
dispatch_async(queue2, ^{
NSLog(@"4--%@", NSThread.currentThread);
});
NSLog(@"-----end-----");
4.3 同步函数 + 串行队列
- 不会开启子线程,所有的任务在当前线程中串行执行
//01 创建并发队列
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_SERIAL);
//02 封装任务,把任务添加到队列
NSLog(@"----start----");
dispatch_sync(queue1, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_sync(queue1, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
dispatch_sync(queue2, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
dispatch_sync(queue2, ^{
NSLog(@"4--%@", NSThread.currentThread);
});
NSLog(@"-----end-----");
4.4 同步函数 + 并发队列
- 不会开启子线程,所有的任务在当前线程中串行执行
//01 创建并发队列
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"----start----");
//02 封装任务,把任务添加到队列
dispatch_sync(queue1, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_sync(queue1, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
dispatch_sync(queue2, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
dispatch_sync(queue2, ^{
NSLog(@"4--%@", NSThread.currentThread);
});
NSLog(@"-----end-----");;
4.5 异步函数 + 主队列
不会开启子线程,所有的任务在主线程中串行执行,
//获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
NSLog(@"----start----");
dispatch_async(queue, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_async(queue, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
dispatch_async(queue, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
NSLog(@"-----end-----");
4.6 同步执行 + 主队列
当在主线程调用([self yncMain]):死锁.
- (void)yncMain {
//获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
//02 封装任务,把任务添加到队列
NSLog(@"----start----");
dispatch_sync(queue, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_sync(queue, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
dispatch_sync(queue, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
NSLog(@"-----end-----");
}
在其他线程中使用
[NSThread detachNewThreadSelector:@selector(asyncMain) toTarget:self withObject:nil];
- 所有任务都是在主线程中执行的,没有开启新的线程(所有放在
主队列中的任务,都会放到主线程中执行)。 - 任务是按顺序执行的(主队列是
串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
syncMain 任务 放到了其他线程里,而 任务 1、任务 2、任务3 都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务 在其他线程中执行到追加 任务 1 到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的 任务1,等 任务1 执行完毕,再接着执行 任务 2、任务 3。所以这里不会卡住线程,也就不会造成死锁问题
5. GCD 线程间的通信
在 iOS 开发过程中,在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
- (void)communication {
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 异步追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
// 回到主线程
dispatch_async(mainQueue, ^{
// 追加在主线程中执行的任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
});
}
6. GCD 的其他方法
6.1 GCD 栅栏方法
//需求:有4个任务,要求开启多条线程来执行这些任务,任务++++++,要求在1,2执行之后执行,要保证该任务执行完之后才能执行后面的3,4任务
//栅栏函数:前面的任务并发执行,后面的任务也是并发执行
//当前面的任务执行完毕之后执行栅栏函数中的任务,等该任务执行完毕后再执行后面的任务
//⚠️ 不能使用全局并发队列
dispatch_queue_t queue = dispatch_queue_create("wwww", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_async(queue, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
//栅栏函数
dispatch_barrier_sync(queue, ^{
NSLog(@"+++++++++++");
});
dispatch_async(queue, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
dispatch_async(queue, ^{
NSLog(@"4--%@", NSThread.currentThread);
});
6.2 GCD 一次性代码
//一次性代码:整个程序运行过程中只会执行一次 + 本身是线程安全
//内部实现原理:判断onceToken的值 == 0 来决定是否执行block中的任务
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"-----once------");
});
6.3 GCD 延时执行方法
//GCD中的延迟执行
/* 参数说明
* 第一个参数:设置时间(GCD中的时间单位是纳秒)
* 第二个参数:队列(决定block中的任务在哪个线程中执行,如果是主队列就是主线程,否在就在子线程)
* 第三个参数:设置任务
* 原理:先等两秒,再把任务提交到队列
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2----%@", NSThread.currentThread);
});
//NSObject的方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.];
//使用NSTimer
[NSTimer scheduledTimerWithTimeInterval:2. target:self selector:@selector(run) userInfo:nil repeats:NO];
6.4 GCD 快速迭代方法
dispatch_queue_t queueu = dispatch_queue_create("qqqq", DISPATCH_QUEUE_CONCURRENT);
/* 参数说明
* 第一个参数:遍历的次数
* 第二个参数:队列
*/
//并发队列:会开启多条子线程和主线程一起并发的执行任务
//主队列:死锁
//普通的串行的队列:和for循环一样
dispatch_apply(100000, queueu, ^(size_t i) {
NSLog(@"dddd%zu - %@", i, NSThread.currentThread);
});
//因为是在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。
//但是 apply---end 一定在最后执行。这是因为 dispatch_apply 方法会等待全部任务执行完毕。
NSLog(@"apply---end");
6.5 GCD 定时器
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//NSTimer中的定时器工作会受到runloop运行模式的影响
//GCD中的定时器是精准的,不受影响
//01 创建定时器对象
/* 参数说明
* 第一个参数:soure的类型 DISPATCH_SOURCE_TYPE_TIMER 定时器
* 第二个参数:对第一个参数的描述
* 第三个参数:更详细的描述
* 第四个参数:队列(GCD-4) 决定代码块(event_handler)在哪个线程中执行(主队列-主线程|非主队列-子线程)
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//02 设置定时器(开始时间|调用间隔|精准度)
/* 参数说明
*
* 第一个参数:定时器对象
* 第二个参数:开始计时的时间 DISPATCH_TIME_NOW 现在开始
* 第三个参数:间隔时间
* 第四个参数:精准度(允许的误差)
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//设置定时源事件处理程序块。
dispatch_source_set_event_handler(timer, ^{
//03 事件回调(定时器执行的任务)
NSLog(@"%@", NSThread.currentThread);
});
//04 启动定时器
dispatch_resume(timer);
self.timer = timer;
6.6 GCD 队列组
1. 队列组等待
//创建队列组
dispatch_group_t group = dispatch_group_create();
//创建队列
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue1, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_group_async(group, queue1, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
dispatch_group_async(group, queue2, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
dispatch_group_async(group, queue2, ^{
NSLog(@"4--%@", NSThread.currentThread);
});
// 等待上面的任务全部完成后,会往下继续执行(阻塞当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"---end----");
2. 队列组通知拦截
//创建队列组
dispatch_group_t group = dispatch_group_create();
//创建队列
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue1, ^{
NSLog(@"1--%@", NSThread.currentThread);
});
dispatch_group_async(group, queue1, ^{
NSLog(@"2--%@", NSThread.currentThread);
});
dispatch_group_async(group, queue2, ^{
NSLog(@"3--%@", NSThread.currentThread);
});
dispatch_group_async(group, queue2, ^{
NSLog(@"4--%@", NSThread.currentThread);
});
//当与调度组关联的所有任务都已完成时,此函数将通知块安排为提交到指定队列。
//才执行 dispatch_group_notify 相关 block 中的任务。
//内部是异步的执行
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"+++++++++%@", NSThread.currentThread);
});
NSLog(@"---end----");
3. 队列组进入/离开
//创建队列组
dispatch_group_t group = dispatch_group_create();
//创建队列
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
//在该方法后面的任务会被队列组监听
//dispatch_group_enter | dispatch_group_leave 必须成对使用
dispatch_group_enter(group);
dispatch_async( queue1, ^{
NSLog(@"1--%@", NSThread.currentThread);
//监听到该任务已经执行完毕
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async( queue1, ^{
NSLog(@"2--%@", NSThread.currentThread);
//监听到该任务已经执行完毕
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async( queue2, ^{
NSLog(@"-3-%@", NSThread.currentThread);
//监听到该任务已经执行完毕
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async( queue2, ^{
NSLog(@"4--%@", NSThread.currentThread);
//监听到该任务已经执行完毕
dispatch_group_leave(group);
});
//当与调度组关联的所有任务都已完成时,此函数将通知块安排为提交到指定队列。
//内部是异步的执行
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"+++++++++%@", NSThread.currentThread);
});
NSLog(@"---end----");
//dispatch_group_enter、dispatch_group_leave 组合,其实等同于dispatch_group_async。
//dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1
//dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。
//当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务。