GCD Reference

144
作者 NinthDay
2016.09.14 23:49 字数 2711

本文是对 Derek Selander 发表的 grand-central-dispatch-in-depth 一文内容整理和补充,并非原创。关于参考都已在文章末尾备注。

1 GCD 术语

1.1 Serial vs. Concurrent 串行 vs. 并发

概念:该术语描述执行当前任务与其他任务之间的关系。串行执行意味着每次只有一个任务被执行;并发执行即同一时间可以有多个任务被执行。

计算机有单核和多核之分,单 CPU 计算机实际为宏观上并行,微观上串行。就冯诺依曼原理来说,微观上只有等待上一条指令执行完毕才会执行下一条指令,任意一个时刻只处理一条指令;而宏观上来讲,我们眼睛所看到的是多个程序在“同时执行”,这又是如何办到呢?很简单,2 个 或 2 个以上的程序交替间隔得到 CPU 处理时间(称之为时间片),给人造成 CPU 在同时处理多个程序的错觉。

注意:概念中并发执行的同一时间 4 个字可以从宏观上理解,当然并发并行是有一定区别的,具体请看Concurrency vs Parallelism 并发与并行一节。

1.2 Synchronous vs. Asynchronous 同步 vs. 异步

概念:术语描述函数执行某个任务后(任务交由 GCD 执行),等待计划任务完成返回称之为同步方式;而异步方式在将任务交由 GCD 执行后立即返回,执行函数中余下的部分代码,若没有则直接退出当前函数。

注意:同步函数将会阻塞当前线程,直到任务执行完毕返回才进行接下来的操作,而异步函数则不会,当然它会很好地完成预定任务。

1.3 Critical Section 临界区

概念:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。

1.4 Race Condition 竞态条件

概念:从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。

多描述基于特定序列或事件执行时机的软件系统以不受控制的方式运行的行为 ,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。

1.5 Deadlock 死锁

概念:两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

简单理解:两个(有时更多)东西——在大多数情况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。

1.6 Thread Safe 线程安全

线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。

1.7 Context Switch 上下文切换

概念:一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。

注意:前面提及单 CPU 计算机微观上是串行执行任务,同一时刻只允许处理单个任务,采用时间调度方式,从宏观上给人造成多个程序同时处理的假象。而程序A 切换到程序 B 之时必须做一些工作:存储当前程序 A 的作业环境(eg.执行到哪了?环境变量 etc.);恢复程序 B 的作业环境开始工作,即所谓的上下文切换。

1.8 Concurrency vs Parallelism 并发与并行

概念:并发和并行从宏观角度来看都是同时处理多个任务。但并发和并行又有区别,如果你理解的同时是指同一个时刻发生,那么称之为两个或多个任务并行执行;若你理解的同时是指同一时间间隔(0.01秒内)发生,那么称之为多个任务并发执行。

并发代码的不同部分可以同时执行,当然,至于怎么发生或是否发生都取决于系统。多核设备会开辟多个线程同时执行代码的不同部分,称之为并行;然而,单核设备如上面所提及的,它只有一个“大脑”,同一时刻只能执行一项任务,想要实现一样的效果,首先必须运行一个线程,执行上下文切换,然后运行另外一个线程或进程,称之为并发。这通常发生地足够快以致给我们并发执行地错觉,如下图所示:


总结:用数学上的集合符号表示并行 ∈ 并发 ,GCD 中我们可以编写代码要求并发执行,但 GCD 会为我们决定哪些代码并行执行却是未知的。并行执行一定是并发执行,而并发执行不一定是并行执行,毕竟单核设备也能通过上下文切换,造成多个任务“同时执行”的假象。

更多:如果你想深入了解任务的并发执行,不妨看看 this excellent talk by Rob Pike

1.9 Queues 队列

GCD 提供 dispatch queues 管理代码块。这些队列通过 FIFO 方式执行你提供给 GCD 的所有任务。FIFOFirst Input First Output 的缩写,是一种传统的按序执行方式,这意味着第一个被添加到队列里的任务将会是队列中第一个开始的任务,而第二个被添加到队列的任务将会是第二个开始,同理队列中其他任务也是如此。但是第二个任务何时开始我们不得而知,唯一能确定的是它将在第一个任务开始之后执行

所有的调度队列(dispatch queues)自身都是线程安全,你能同时在多个线程访问它们。 GCD 的优点显而易见的,前提是你必须了解调度队列如何为你自己代码的不同部分提供线程安全。选择正确类型的调度队列和调度函数来提交任务是至关重要的。

GCD 提供了两种调度队列:串行队列并发队列

1.9.1 Serial Queues 串行队列

串行队列中加入的任务一次有且仅有一个被执行,只有当前一个任务执行完毕,后一个任务才能开始,至于什么时候开始,这取决于 GCD,如下图所示:


这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。

由于在串行队列中不会有两个任务并发运行,因此不会出现同时访问临界区的风险;相对于这些任务来说,这就从竞态条件下保护了临界区。所以如果访问临界区的唯一方式是通过提交到调度队列的任务,那么你就不需要担心临界区的安全问题了。

1.9.2 Concurrent Queues 并发队列

并发队列中的任务能得到的保证是它们会按照被添加的顺序开始执行,但这就是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。再说一遍,这完全取决于 GCD 。

下图展示了一个示例任务执行计划,GCD 管理着四个并发任务:


注意到队列中 4 个任务执行顺序即为添加到队列的顺序,但是 Block1 并未在 Block0 开始后立即执行,而是等待一段时间后开始(图中是在 Block0 执行完毕后开始),而 Block1、Block2 和 Block3 按照顺序立即执行。

何时开始一个 Block 完全取决于 GCD 。如果一个 Block 的执行时间与另一个重叠,也是由 GCD 来决定是否分配一个 CPU 核单独处理,否则就用上下文切换的方式来执行不同的 Block 。

GCD 提供了至少五个特定的队列,可根据队列类型选择使用。

1.9.3 Queue Types 队列类型

队列 队列类型 说明
主队列(main queue) 串行 保证所有的任务都在主线程执行,而主线程是唯一用于 UI 更新的线程。此外还用于发送消息给视图或发送通知。
四个全局调度队列(high、default、low、background) 并发 Apple 的接口也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务
自定义队列 串行 or 并发 1. 多个任务以串行方式执行,但又不想在主线程中;2. 多个任务以并行方式执行,但不希望队列中有其他系统的任务干扰。

2 API 接口介绍

3 Demo 讲解

4 源码分析

文献

日常笔记