什么是GCD?
Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其他的对称多处理系统的系统。这建立在任务并行执行的线程池模式的基础上的。它首次发布在Mac OS X 10.6 ,iOS 4及以上也可用。
GCD是一套底层API,提供了一种新的方法来进行并发程序编写。从基本功能上讲,GCD有点像NSOperationQueue,他们都允许程序将任务切分为多个单一任务然后提交至工作队列来并发地或者串行地执行。GCD比之NSOpertionQueue更底层更高效,并且它不是Cocoa框架的一部分。
除了代码的平行执行能力,GCD还提供高度集成的事件控制系统。可以设置句柄来响应文件描述符、mach ports(Mach port 用于 OS X上的进程间通讯)、进程、计时器、信号、用户生成事件。这些句柄通过GCD来并发执行。
GCD的API很大程度上基于block,当然,GCD也可以脱离block来使用,比如使用传统c机制提供函数指针和上下文指针。实践证明,当配合block使用时,GCD非常简单易用且能发挥其最大能力。
为何使用GCD?
GCD提供很多超越传统多线程编程的优势:
易用:
GCD
比之thread
更简单易用。由于GCD
基于work unit
而非像thread
那样基于运算,所以GCD
可以控制诸如等待任务结束、监视文件描述符、周期执行代码以及工作挂起等任务。基于block
的血统导致它能极为简单地在不同代码作用域之间传递上下文。效率:
GCD
被实现得如此轻量和优雅,使得它在很多地方比之专门创建消耗资源的线程更实用且快速。这关系到易用性:导致GCD
易用的原因有一部分在于你可以不用担心太多的效率问题而仅仅使用它就行了。性能:
GCD
自动根据系统负载来增减线程数量, 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能,这就减少了上下文切换以及增加了计算效率。
GCD常用术语
Serial vs. Concurrent 串行 vs. 并发
- 这些术语描述当任务相对于其它任务被执行,任务串行执行就是每次只有一个任务被执行,任务并发执行就是在同一时间可以有多个任务被执行。
Synchronous vs. Asynchronous 同步 vs. 异步
同步执行:只要是同步执行的任务,都会在当前线程执行,不会另开线程。如果是 同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
异步执行:只要是异步执行的任务,都会另开线程,在别的线程执行。如果是 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。
Critical Section 临界区
- 就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很可能会变质,出现线程安全问题。
Race Condition 竞态条件
- 这种状况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。
Deadlock 死锁
- 两个(有时更多)东西——在大多数情况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。
Thread Safe 线程安全
- 线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。
Context Switch 上下文切换
- 一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。
Dispatch Queues 队列
GCD 提供有
dispatch queues
来处理代码块,这些队列管理你提供给 GCD 的任务并用FIFO
顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行的访问它们。 GCD 的优点是显而易见的,即当你了解了调度队列如何为你自己代码的不同部分提供线程安全。关于这一点的关键是选择正确类型的调度队列和正确的调度函数来提交你的工作。
GCD中有三种队列类型:
1、Main queue:与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用
dispatch_get_main_queue()
来获得。因为main queue是与主线程相关的,所以这是一个串行队列
。2、Global queues:全局队列是
并发队列
,并由整个进程共享。进程中存在三个全局队列:高
、中(默认)
、低
三个优先级队列。可以调用dispatch_get_global_queue
函数传入优先级来访问队列。3、用户队列:用户队列 (GCD并不这样称呼这种队列, 但是没有一个特定的名字来形容这种队列,所以我们称其为用户队列) 是用函数
dispatch_queue_create
创建的队列, 这些队列是串行的。正因为如此,它们可以用来完成同步机制, 有点像传统线程中的mutex。
GCD常用方法
获取队列
// 并行队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async 异步执行
为了避免界面在处理耗时的操作时卡死,比如读取网络数据,IO,数据库读写等,我们会在另外一个线程中处理这些操作,然后通知主线程更新界面。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
// 更新界面
});
});
dispatch_once 一次执行
dispatch_once
的作用正如其名:对于某个任务执行一次,且只执行一次,其被广泛使用在单例、缓存等代码中,用以保证在初始化时执行一次某任务。以下例子将展示创建单例:
+ (instancetype)defaultInfoCenter {
static InfoCenter *infoCenter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
infoCenter = [[InfoCenter alloc] init];
});
return infoCenter;
}
dispatch_after 延迟执行
dispatch_after
能让我们添加进队列的任务延时执行,比如想让一个Block在主队列中延迟5s
执行:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"在5s后执行");
});
注意:
NSEC_PER_SEC
表示的是秒数,它还提供了NSEC_PER_MSEC
表示毫秒。dispatch_get_main_queue()
表示在主队列中执行。
dispatch_apply
dispatch_apply
会将一个指定的block执行指定的次数。如果要对某个数组中的所有元素执行同样的block的时候,这个函数就显得很有用了,用法很简单,指定执行的次数以及Dispatch Queue,在block回调中会带一个索引,然后就可以根据这个索引来判断当前是对哪个元素进行操作,dispatch_apply
是同步函数,会阻塞当前线程直到所有循环迭代执行完成。当提交到并发queue
时,循环迭代的执行顺序是不确定的:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(5, globalQueue, ^(size_t index) {
NSLog(@"index is %zu.", index);
});
NSLog(@"complete.");
由于是Concurrent Dispatch Queue
,不能保证哪个索引的元素是先执行的,但是completed.
一定是在最后打印,因为dispatch_apply
函数是同步的,执行过程中会使线程在此处等待,所以一般我们应该在一个异步线程里使用dispatch_apply
函数:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
dispatch_apply(5, globalQueue, ^(size_t index) {
NSLog(@"index is %zu.", index);
});
NSLog(@"complete.");
});
NSLog(@"在'dispatch_apply'之前.");
dispatch_suspend / dispatch_resume
某些情况下,我们可能会想让Dispatch Queue
暂时停止一下,然后在某个时刻恢复处理,这时就可以使用dispatch_suspend
以及dispatch_resume
函数:
// 暂停
dispatch_suspend(globalQueue)
// 恢复
dispatch_resume(globalQueue)
注意:暂停时,如果已经有block正在执行,那么不会对该block的执行产生影响。
dispatch_suspend
只会对还未开始执行的block产生影响。
dispatch_group
dispatch_group_async
可以实现监听一组
任务是否完成,完成后通知执行其他的操作。这个方法很有用,比如你执行三个下载任务,当三个任务都下载完成后才通知界面进行刷新。下面是一段例子代码:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, globalQueue, ^{
NSLog(@"下载任务‘1’执行完毕!");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"下载任务‘2’执行完毕!");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"下载任务‘3’执行完毕!");
});
dispatch_group_notify(group, globalQueue, ^{
NSLog(@"下载任务完毕,刷新界面!");
});
2015-09-07 18:57:47.647 GCDTest[3257:312802] 下载任务‘3’执行完毕!
2015-09-07 18:57:47.647 GCDTest[3257:312799] 下载任务‘1’执行完毕!
2015-09-07 18:57:47.647 GCDTest[3257:312800] 下载任务‘2’执行完毕!
2015-09-07 18:57:47.648 GCDTest[3257:312800] 下载任务完毕,刷新界面!
注意:输出的顺序与添加进队列的顺序无关,因为队列是
Concurrent Dispatch Queue
,但“下载任务完毕,刷新界面!”的输出一定是在最后的。
除了使用dispatch_group_notify
函数可以得到最后执行完的通知外,还可以使用dispatch_group_wait
函数。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, globalQueue, ^{
NSLog(@"下载任务‘1’执行完毕!");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"下载任务‘2’执行完毕!");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"下载任务‘3’执行完毕!");
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"下载任务完毕,刷新界面!");
需要注意的是,dispatch_group_wait
实际上会使当前的线程处于等待的状态,也就是说如果是在主线程执行dispatch_group_wait
,在上面的Block执行完之前,主线程会处于卡死的状态。可以注意到dispatch_group_wait
的第二个参数是指定超时的时间,如果指定为DISPATCH_TIME_FOREVER
(如上面这个例子)则表示会永久等待,直到上面的Block全部执行完,除此之外,还可以指定为具体的等待时间,根据dispatch_group_wait
的返回值来判断是上面block执行完了还是等待超时了。
最后,同之前创建dispatch_queue
一样,如果是在OS X 10.8
或iOS 6
以及之后版本中使用,Dispatch Group将会由ARC自动管理,如果是在此之前的版本,需要自己手动释放。