Ps:这篇文章是之前收集的好多关于 GCD 的资料,还有一些自己平常时候的使用总结下来的, 誊抄以备. 因为在 ios 的开发中关于线程,我好像只用 GCD.
GCD 是基于 C 的 API, 和 OC 的调用迥异.但是 apple也是把 GCD 封装了一下,变成 OC 的语法, 在效率上肯定有所损失.
什么是 GCD
GCD 是 libdispatch
的市场名称,而 libdispatch 作为 Apple 的一个库,为并发代码在多核硬件(跑 iOS 或 OS X )上执行提供有力支持。它具有以下优点:
- GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
- GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
- GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。
本教程假设你对 Block 和 GCD 有基础了解。如果你对 GCD 完全陌生,先看看 iOS 上的多线程和 GCD 入门教程 学习其要领。
GCD 术语
要理解 GCD ,你要先熟悉与线程和并发相关的几个概念。这两者都可能模糊和微妙,所以在开始 GCD 之前先简要地回顾一下它们。
Serial vs. Concurrent 串行 vs. 并发
这些术语描述当任务相对于其它任务被执行,任务串行执行就是每次只有一个任务被执行,任务并发执行就是在同一时间可以有多个任务被执行。
虽然这些术语被广泛使用,本教程中你可以将任务设定为一个 Objective-C 的 Block 。不明白什么是 Block ?看看iOS 5 教程中的如何使用 Block 。实际上,你也可以在 GCD 上使用函数指针,但在大多数场景中,这实际上更难于使用。Block 就是更加容易些!
Synchronous vs. Asynchronous 同步 vs. 异步
在 GCD 中,这些术语描述当一个函数相对于另一个任务完成,此任务是该函数要求 GCD 执行的。一个_同步_函数只在完成了它预定的任务后才返回。
一个_异步_函数,刚好相反,会立即返回,预定的任务会完成但不会等它完成。因此,一个异步函数不会阻塞当前线程去执行下一个函数。
注意——当你读到同步函数“阻塞(Block)”当前线程,或函数是一个“阻塞”函数或阻塞操作时,不要被搞糊涂了!动词“阻塞”描述了函数如何影响它所在的线程而与名词“代码块(Block)”没有关系。代码块描述了用 Objective-C 编写的一个匿名函数,它能定义一个任务并被提交到 GCD 。
译者注:中文不会有这个问题,“阻塞”和“代码块”是两个词。
Critical Section 临界区
就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很可能会变质(译者注:它的值不再可信)。
Race Condition 竞态条件
这种状况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。
Deadlock 死锁
两个(有时更多)东西——在大多数情况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。
Thread Safe 线程安全
线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是NSDictionary
。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary
就不是线程安全的,应该保证一次只能有一个线程访问它。
Context Switch 上下文切换
一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。
Concurrency vs Parallelism 并发与并行
并发和并行通常被一起提到,所以值得花些时间解释它们之间的区别。
并发代码的不同部分可以“同步”执行。然而,该怎样发生或是否发生都取决于系统。多核设备通过并行来同时执行多个线程;然而,为了使单核设备也能实现这一点,它们必须先运行一个线程,执行一个上下文切换,然后运行另一个线程或进程。这通常发生地足够快以致给我们并发执行地错觉,如下图所示:
虽然你可以编写代码在 GCD 下并发执行,但 GCD 会决定有多少并行的需求。并行_要求_并发,但并发并不能_保证_并行。
Ps:有些博客中将并发队列说成 并行队列,这是不对的。因为并行是多个事件在同一时刻发生,而并发是多个事件在同一时间间隔发生;并行完全依赖处理器的核数。而并发才能充分的利用处理器的每一个核,以达到最高的处理性能。并行一定并发,但并发不一定并行, 但是并发能发挥多核 CPU 的最大性能.
Queues 队列
GCD 提供有 dispatch queues
来处理代码块,这些队列管理你提供给 GCD 的任务并用 FIFO 顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。
所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行的访问它们。当你了解了调度队列如何为你自己代码的不同部分提供线程安全后,GCD的优点就是显而易见的。关于这一点的关键是选择正确_类型_的调度队列和正确的_调度函数_来提交你的工作。
在本节你会看到两种调度队列,都是由 GCD 提供的,然后看一些描述如何用调度函数添加工作到队列的例子。
Serial Queues 串行队列
串行队列中的任务一次执行一个,每个任务只在前一个任务完成时才开始。而且,你不知道在一个 Block 结束和下一个开始之间的时间长度,如下图所示:
这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。
由于在串行队列中不会有两个任务并发运行,因此不会出现同时访问临界区的风险;相对于这些任务来说,这就从竞态条件下保护了临界区。所以如果访问临界区的唯一方式是通过提交到调度队列的任务,那么你就不需要担心临界区的安全问题了。
Concurrent Queues 并发队列
在并发队列中的任务能得到的保证是它们会按照被添加的顺序开始执行,但这就是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。再说一遍,这完全取决于 GCD 。
下图展示了一个示例任务执行计划,GCD 管理着四个并发任务:
注意 Block 1,2 和 3 都立马开始运行,一个接一个。在 Block 0 开始后,Block 1等待了好一会儿才开始。同样, Block 3 在 Block 2 之后才开始,但它先于 Block 2 完成。
何时开始一个 Block 完全取决于 GCD 。如果一个 Block 的执行时间与另一个重叠,也是由 GCD 来决定是否将其运行在另一个不同的核心上,如果那个核心可用,否则就用上下文切换的方式来执行不同的 Block 。
有趣的是, GCD 提供给你至少五个特定的队列,可根据队列类型选择使用。
Queue Types 队列类型
首先,系统提供给你一个叫做 主队列(main queue)
的特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发生消息给UIView
或发送通知的。
系统同时提供给你好几个并发队列。它们叫做 全局调度队列(Global Dispatch Queues)
。目前的四个全局队列有着不同的优先级:background
、low
、default
以及high
。要知道,Apple 的 API 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。
最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有_五个_队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。
队列不等于线程。 作为开发者的我们,只是将Block添加进合适的GCD队列,真正的线程的调度是由系统完成的;无论同步(sync)还是异步(async)向主队列提交Block,最终Block都是在主线程中执行;同步(sync)往非主队列中提交Block,会在当前线程中执行; 如果是异步(async)往非主队列中提交Block,则会在分线程中执行。
队列组合
1)串行队列 + 同步组合(常用)
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);
});
说明1:串行队列 (自己创建的串行线程)+ 同步组合下,不会新建线程,依然在当前线程上执行任务。不可以在主线程中使用sync方法,会造成死锁。
说明2:比较常用,同步锁的替代方法。
2)串行队列 + 异步组合
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]); });
串行队列 + 异步组合结果.png
说明:串行队列(无论是自己创建的,还是获取主队列) + 异步组合下,会新建线程,但只开启一条线程;
3)并发队列 + 同步组合
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);dispatch_sync(concurrentQueue, ^{ NSLog(@"并发队列 + 同步1:%@",[NSThread currentThread]); }); dispatch_sync(concurrentQueue, ^{ NSLog(@"并发队列 + 同步2:%@",[NSThread currentThread]); }); dispatch_sync(concurrentQueue, ^{ NSLog(@"并发队列 + 同步3:%@",[NSThread currentThread]); });
并发队列 + 同步组合结果.png
说明: 并发队列(无论是自己创建的,还是获取全局队列) + 同步组合下,并没有新建线程,任务依然在当前线程上执行。
4)并发队列 + 异步组合(常用)
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);
});
并发队列 + 异步组合结果.png
说明:并发队列(无论是自己创建的,还是获取全局队列) + 异步组合下,会新建线程,iOS 系统中可以开多条线程。
| 同步 | 异步 |
---|---|---|
串行队列 | 1、不会新建线程,依然在当前线程上执行任务;2、类似同步锁,是同步锁的替代方案 | 1、会新建线程,但只开启一条线程;2、每次使用 dispatch_queue_create创建串行队列,就会创建一条新线程;多次创建,会创建多条线程,多条线程间并发执行。 |
并发队列 | 不会新建线程,依然在当前线程上执行任务 | 1、会新建线程,可以开多条线程;2、iOS7-SDK 时代一般是5、6条, iOS8-SDK 以后可以50、60条 |
总结1:不可以在主线程中使用sync方法,否则会造成死锁。
总结2:串行队列 + 同步组合 可以替代同步锁;
总结3:为了提高效率,如多线程下载图片等,并发队列 + 异步比较常用。