前文中,我们介绍了多线程的基本概念和多线程编程实现的两种方式,本文,我们介绍一下最后一种多线程编程工具,也是最重要的一种:GCD。
1. GCD简介
1.1 什么是GCD
GCD全称是Grand Central Dispatch,可译为“牛逼的中枢调度器” ,纯C语言,提供了非常多强大的函数。GCD为我们编写代码提供了非常绝佳的体验。它是苹果公司为在多核心设备上实现多线程充分利用多核优势提供的解决方案,在很多技术中,如 RunLoop GCD都扮演了重要的角色。GCD全称Grand Central Dispatch,直译为调度中心,完全基于C语言编写。使用GCD,不需要管理线程,线程管理完全托管给GCD。把线程想象成的火车,我们只需要提交我们的运送任务,GCD会帮我们在合适的火车上运行任务。多线程编程变得如此简单,但对于初学者来讲,GCD学习会有一定难度。因为是纯C编写。所以没有对象的概念,学习GCD你可以忘掉封装继承多态等等概念。
1.2 GCD的主要优势
GCD提供很多超越传统多线程编程的优势:
- 易用: GCD比之thread跟简单易用,程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。由于GCD基于work unit而非像thread那样基于运算,所以GCD可以控制诸如等待任务结束、监视文件描述符、周期执行代码以及工作挂起等任务。基于block的血统导致它能极为简单得在不同代码作用域之间传递上下文。
- 效率: GCD是苹果公司为多核的并行运算提出的解决方案,GCD会自动利用更多的CPU内核(比如双核、四核)。GCD被实现得如此轻量和优雅,使得它在很多地方比之专门创建消耗资源的线程更实用且快速。这关系到易用性:导致GCD易用的原因有一部分在于你可以不用担心太多的效率问题而仅仅使用它就行了。
- 性能: GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。GCD自动根据系统负载来增减线程数量,这就减少了上下文切换以及增加了计算效率。
1.3 使用GCD需要注意的几点
- GCD存在于libdispatch.dylib这个库中,这个调度库包含了GCD的所有的东西,但任何IOS程序,默认就加载了这个库,在程序运行的过程中会动态的加载这个库,不需要我们手动导入。
- GCD是纯C语言的,因此我们在编写GCD相关代码的时候,面对的函数,而不是方法。
- GCD中的函数大多数都以dispatch开头。
1.4 GCD中有2个核心概念
- 队列:用来存放任务
- 任务:执行什么操作
1.5 GCD的使用的2个步骤
(1)定制队列
(2)提交任务,确定想做的事情
注意:将任务添加到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行
提示:任务的取出遵循队列的FIFO原则:先进先出,后进后出
2. 创建队列
GCD中的一个重要组成部分就是队列,我们把各种任务提交给队列,队列根据它本身的类型以及当前系统的状态,添加到不同的线程中执行任务。线程的创建和管理都有GCD 本身完成,不需要我们参与。系统提供了很多定义好的队列:只管理主线程的main_queue,全局并行的 globle_queue。同时,我们也可以自定义自己的队列queue。
GCD的队列可以分为2大类型:
1. 并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)并发功能只有在异步(dispatch_async)函数下才有效
2. 串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)所有线程串行,或者只有一个线程,任务依次执行。
queue的类型为 dispatch_queue_t。
创建函数是:dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
第一个参数代表queue的名字,注意是char型指针,并非NSString;
第二个参数表示queue的类型,
- DISPATCH_QUEUE_SERIAL表示串行 queue。
- DISPATCH_QUEUE_CONCURRENT表 示并行queue。
2.1 串行队列
GCD中获得串行有两种途径
- 使用主队列(跟主线程相关联的队列)
主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行。提交给main queue的任务可能不会立马被执行,而是在主线程的Run Loop检测到有dispatch 提交过来的任务时才会执行。
使用dispatch_get_main_queue()函数获得主队列
注意:如果把任务放到主队列中进行处理,那么不论处理函数是异步还是同步,都不会开辟新的线程。
示例:dispatch_queue_t queue = dispatch_get_main_queue();
- 使用dispatch_queue_create函数创建串行队列
说明:dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr); // 队列名称, 队列属性,一般用NULL即可,也可以设置参数DISPATCH_QUEUE_SERIAL
示例:dispatch_queue_t queue = dispatch_queue_create(“队列”, NULL); // 创建
注意:非ARC需要释放手动创建的队列,dispatch_release(queue);
2.2 并发队列
GCD中同样提供了两种途径获得串行
- 使用全局队列
GCD默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建。也就是说我们可以直接提交给这个queue,任务会在非主线程的其他线程执行。
使用dispatch_get_global_queue函数获得全局的并发队列
说明:dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority,unsigned long flags);
示例: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 // 后台
第二个参数参数是留给以后用的,暂时用不上,传个0。
获得全局并发Dispatch Queue (concurrent dispatch queue)
1. 并发dispatch queue可以同时并行地执行多个任务,不过并发queue仍然按先进先出的顺序来启动任务。并发queue会在之前的任务完成之前就出列下一个任务并开始执行。并发queue同时执行的任务数量会根据应用和系统动态变化,各种因素包括:可用核数量、其它进程正在执行的工作数量、其它串行dispatch queue中优先任务的数量等。
2. 系统给每个应用提供三个并发dispatch queue,整个应用内全局共享,三个queue的区别是优先级。你不需要显式地创建这些queue,使用dispatch_get_global_queue函数来获取这三个queue
第一个参数用于指定优先级,分别使用DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW两个常量来获取高和低优先级的两个queue;第二个参数目前未使用到,默认0即可
3. 虽然dispatch queue是引用计数的对象,但你不需要retain和release全局并发queue。因为这些queue对应用是全局的,retain和release调用会被忽略。你也不需要存储这三个queue的引用,每次都直接调用dispatch_get_global_queue获得queue就行了。
- 使用dispatch_queue_create函数创建并行队列
和获取串行队列一样,我们同样可以使用dispatch_queue_create函数创建并行队列,区别在于填入的第二个单数和获取串行队列的有所不同。
说明:dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr); // 队列名称, 队列属性设置参数为DISPATCH_QUEUE_CONCURRENT
示例:dispatch_queue_t queue = dispatch_queue_create(“队列”, DISPATCH_QUEUE_CONCURRENT);
注意:非ARC需要释放手动创建的队列,dispatch_release(queue);
2.3 queue的内存管理
ARC环境下,不需要手动编写代码。 非ARC环境下需要使用 dispatch_retain()和 dispatch_release() 管理queue的引用计数。原则如同NSObject对象,计数为0时销毁queue。
代码实例:
dispatch_queue_t q_1 = dispatch_queue_create("task3.queue.1",DISPATCH_QUEUE_SERIAL);
dispatch_release(q_1);
3. 提交任务
GCD中有2个用来执行任务的函数,两个函数的作用就是把右边的参数(任务)提交给左边的参数(队列)进行执行。
1. 用同步的方式执行任务 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
2. 用异步的方式执行任务 dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
参数说明:queue,任务执行的队列
block,需要执行任务
同步和异步的区别:
- 同步:在当前线程中执行,会等待任务完成,卡死当前线程
- 异步:在另一条线程中执行,不需要等待任务完成,不会对当前线程产生影响
3.1 同步提交
同步提交函数为dispatch_sync(),两个参数:
- 第一个参数表示提交到的queue
- 第二个参数表示任务详情
这里用block的方式描述一个任务,原因很简单,Block也是纯C实现的,而平时使用的Invocation或者target+selector方式都是面向对象的。
注意,同步提交任务后,先执行完block,然后dispatch_sync()返回,这时候如果同步提交的任务过长会导致主线程的卡死。
3.2 异步提交
异步提交函数为dispatch_async(),两个参数:
- 第一个参数表示提交到的queue
- 第二个参数表示任务详情
异步提交任务后,dispatch_async()函数直接返回,无需等待block执行结束。不会导致主线程的卡死。
3.3 同时提交多次任务
同时提交多个任务给queue的函数很简单:
dispatch_apply(size_titerations,dispatch_queue_tqueue,void(^block)(size_t));
三个参数分别是任务数,目标queue,任务描述
这里注意描述任务的block会重复多次调用,每次会给我们一个参数,表示任务次序。多个任务的执行顺序取决于添加队列queue的形式,如果目标queue是串行的,那么任务会依次执行,如果queue是并行的,那么任务会并发的执行,打印的顺序就会被打乱。
3.4 死锁
同步提交在某种情况下会造成死锁,即卡死。 示例1:
dispatch_queue_t mainQ = dispatch_get_main_queue();
dispatch_sync(mainQ, ^{
NSLog(@"----");
});
NSLog(@"OK");
示例2:
dispatch_queue_t q_1 = dispatch_queue_create("task3.queue.1",DISPATCH_QUEUE_SERIAL);
dispatch_async(q_1, ^{
NSLog(@"current is in q_1");
// q_1 blcok q_1 q_1 dispatch_sync
block block dispatch_sync
dispatch_sync(q_1, ^{
NSLog(@"this is sync ");
});
NSLog(@"this is sync ????");
});
综合上面两个示例代码,结论显而易见:在一个串行队列执行的代码中,如果向此队列同步提交一个任务,会造成死锁。为了避免出现死锁的情况,要求我们避免在串行队列中同步提交任务给本身的队列。
3.5 各种队列的执行效果
3.6 补充说明
学习GCD时,有4个术语比较容易混淆:同步、异步、并发、串行。
同步和异步决定了要不要开启新的线程
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
并发和串行决定了任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
3.7 代码示例
(1)用异步函数往并发队列中添加任务
//1.获得全局的并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2.添加任务到队列中,就可以执行任务
//异步函数:具备开启新线程的能力
dispatch_async(queue, ^{
NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);
总结:同时开启三个子线程
(2)用异步函数往串行队列中添加任务
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);
//创建串行队列
dispatch_queue_t queue= dispatch_queue_create("wendingding", NULL);
//第一个参数为串行队列的名称,是c语言的字符串
//第二个参数为队列的属性,一般来说串行队列不需要赋值任何属性,所以通常传空值(NULL)
//2.添加任务到队列中执行
dispatch_async(queue, ^{
NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
总结:会开启线程,但是只开启一个线程
(3)用同步函数往并发队列中添加任务
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);
//创建串行队列
dispatch_queue_t queue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2.添加任务到队列中执行
dispatch_sync(queue, ^{
NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片3----%@",[NSThread currentThread]);
});
}
总结:不会开启新的线程,并发队列失去了并发的功能
(4)用同步函数往串行队列中添加任务
NSLog(@"用同步函数往串行队列中添加任务");
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);
//创建串行队列
dispatch_queue_t queue= dispatch_queue_create("wendingding", NULL);
//2.添加任务到队列中执行
dispatch_sync(queue, ^{
NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片3----%@",[NSThread currentThread]);
});
}
总结:不会开启新的线程
(5)补充
补充:队列名称的作用:
将来调试的时候,可以看得出任务是在哪个队列中执行的。
4. queue的暂停和继续
我们可以使用dispatch_suspend函数暂停一个queue以阻止它执行尚未block对象,使用 dispatch_resume函数继续dispatch queue。挂起和继续是异步的,而且只在执行block之间(比如在执行一个新的block之前或之后)生效。挂起一个queue不会导致正在执行的block停止。特别强调,需要我们保证挂起队列和重启队列的函数成对调用。
在非ARC中使用时需要注意:调用dispatch_suspend会增加queue的引用计数,调用 dispatch_resume则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。
// 挂起与重启任务
dispatch_suspend(globe_queue);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
dispatch_resume(globe_queue);
});
5. Dispatch Group
什么是dispatch group,就像我们在NSOperation中添加依赖一样,如果我们一个任务需要等待其他一些任务完成才能执行时,我们使用dispatch group是最轻松的解决方式。
使用队列组可以让图片1和图片2的下载任务同时进行,且当两个下载任务都完成的时候回到主线程进行显示。
5.1 设置任务执行顺序
GCD设置任务执行顺序是通过Dispatch Group实现的。我们以吃火锅为例:
- 首先创建group 与任务队列
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globleQ = dispatch_get_global_queue(0, 0);
- 提交任务,并且把任务添加到group中
dispatch_group_async(group, globleQ, ^{});
注意:提交任务到group是没有同步提交的,只有异步提交
- 提交最终任务到group,同样为异步提交,
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{});
5.2 时间延迟
- dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
使当前线程堵塞,一直等待group所有任务结束:
- dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (300 *NSEC_PER_SEC));
- dispatch_group_wait(group, time); NSLog(@”“);
也可以自定义超时时间,只等待一定的时间之后group内的任务没有执行的直接停止
6. GCD的常用方法
6.1 延迟执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//延迟执行的方法
});
说明:在5秒钟之后,执行block中的代码段。
参数说明:
6.2 一次性执行(常用与单例设置)
使用dispatch_once一次性代码
使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
整个程序运行过程中,只会执行一次。
7. 小结
说明:同步函数不具备开启线程的能力,无论是什么队列都不会开启线程;异步函数具备开启线程的能力,开启几条线程由队列决定(串行队列只会开启一条新的线程,并发队列会开启多条线程)。
同步函数
(1)并发队列:不会开线程
(2)串行队列:不会开线程
异步函数
(1)并发队列:能开启N条线程
(2)串行队列:开启1条线程
补充: 在非ARC工程中,凡是函数中,各种函数名中带有create\copy\new\retain等字眼,都需要在不需要使用这个数据的时候进行release。
GCD的数据类型在ARC的环境下不需要再做release。
CF(core Foundation)的数据类型在ARC环境下还是需要做release。
异步函数具备开线程的能力,但不一定会开线程