虽然 GCD 已经出现过一段时间了,但不是每个人都明了其主要内容。这是可以理解的;并发一直很棘手,而 GCD 是基于 C 的 API ,它们就像一组尖锐的棱角戳进 Objective-C 的平滑世界。我们将分两个部分的教程来深入学习 GCD 。
在这两部分的系列中,第一个部分的将解释 GCD 是做什么的,并从许多基本的 GCD 函数中找出几个来展示。在第二部分,你将学到几个 GCD 提供的高级函数。
本文翻译自 http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1
什么是GCD
GCD是libdispatch
的市场名称,而libdispatch作为Apple的一个库,为并发代码在多核硬件(跑iOS或OSX)上执行提供有力支持。它具有以下优点:
- GCD能通过推迟昂贵计算任务并在后台运行他们来改善你的应用的响应性能
- GCD提供一个易于使用的并发模型而不仅仅是锁和线程,以帮助我们避开并发陷进。
- GCD具有常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。
本教程假设你对Block合GCD有基础了解。如果你对GCD完全陌生,先看看iOS上德多线程和GCD入门教程 学习其要领。
GCD术语
要理解GCD,你要熟悉与线程和并发相关的几个概念。这两者都可能模糊和微妙,所以在开始GCD之前先简要地回顾以下它们。
Serial vs. Concurrent 串行 vs. 并行
这些术语描述当任务相对于其它任务被执行,任务串行执行就是每次只有一个任务被执行,任务并发执行就是在同一时间可以有多个任务被执行。
虽然这些术语被广泛使用,本教程中你可以将任务设定为一个Objective-C的Block。不明白什么是Block?看看iOS5教程中的如何使用Block。实际上,你也可以在GCD上使用函数指针,但在大多数场景中,这实际上更难于使用。Block就是更加容易些!
Synchonous vs. Asynchronous 同步 vs. 异步
在GCD中,这些术语描述当一个函数相对于另外一个任务完成,此任务是该函数要求GCD执行的。一个同步函数只在完成了它预定的任务后才返回。
一个异步函数,刚好相反,会立即返回,预定的任务会完成但不会等它完成。因此,一个异步函数不会阻塞当前线程去执行下一个函数。
注意--当你读到同步函数“阻塞(Block)”当前线程,或函数是一个“阻塞”函数或阻塞操作时,不要被搞糊涂了!动词“阻塞”描述了函数如何影响它所在的线程而与名词“代码块(Block)”没有关系。代码块描述了用Objective-C编写的一个匿名函数,它能定义一个任务并被提交到GCD。
译者注:中文不会有这个问题,“阻塞”和“代码块”是两个词。
Critical Section 临界区
就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很肯能会变质(译者注:它的值不再可信)。
Race Condition 竞态条件
这种状况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。
Deadlock 死锁
两个(有时更多)东西--在大多数情况下,是线程--所谓的死锁是指他们都卡住了,并等待对方完成或执行其他操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。
Thread Safe 线程安全
线程安全的代码能在多线程或并发任务中被安全调用,而不会导致任何问题(数据算坏,奔溃,等)。线程不安全的代码在摸个时刻只能在一个上下文中运行。一个线程安全代码的例子是NSDictionary
。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary
就不是线程安全的,应该保证一次只能有一个线程访问它。
Context Switch 上下文切换
一个上下文切换指当你在当个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。
Concurrency vs. Parallelism 并发与并行
并发和并行通常被一起提到,所以值得花些时间解释他们之间的区别。
并发代码的不同部分可以“同步”执行。然而,改怎样发生或是否发生都决定于多系统。多核设备通过并行来同事执行多个线程;然而,为了使单核设备也能实现这一点,他们必须先运行一个线程,执行一个上下文切换,然后运行另一个线程或进程。这通常发生得足够快以致给我们并发执行的错觉,如下图所示:
p1
虽然你可以编写代码在GCD下并发执行,但GCD会决定有多少并行的需求。并行要求并发,但并发并不能保证并行。
更深入的观点是并发实际上是关于构造。当你在脑海中用GCD编写代码,你组织你的代码来暴露同时运行的多个工作片段,以及不能同时运行的那些。如果你想深入此主题,看看这个由Rob Pike做的精彩的讲座
Queues 队列
GCD提供有dispatch queues
来处理代码块,这些队列管理提供给GCD的任务并用FIFO顺序执行这些任务。这就保证了第一个被添加到队列里地任务回事队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此知道队列的终点。
所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行地访问他们。当你了解了调度队列如何为自己代码的不同部分提供线程安全后,GCD的优点就是显而易见的。关于这一点的关键是选择正确类型的调度队列和正确地调度函数来提交你的工作。
在本节你会看到两种调度队列,都是由GCD提供的,然后看一些描述如何用调度函数添加工作到队列的例子。
Serial Queues 串行队列
串行队列中得任务一次执行一个,每个任务只在前一个任务完成时才开始。而且,你不知道在一个Block结束和下一个开始之间的时间长度,如下图所示:
p2
这些任务执行的时机受到GCD的控制;唯一能确保的事情是GCD一次只能执行一个任务,并且按照我们添加到队列的顺序来执行。
由于在串行队列中不会有两个任务并发运行,因此不会出新同时访问临界区的风险;相对于这些任务来说,这就从竞态条件下保护了临界区。所以如果访问临界区的唯一方式是通过提交到调度队列的任务,那么你就不需要担心临界区安全问题了。
Concurrent Queues 并发队列
在并发队列中得任务能得到的保证是它们会按照被添加的顺序开始执行,但这既是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少Block在运行。再说一遍,这完全取决于GCD。
下图展示了一个示例任务执行计划,GCD管理者四个并发任务:
p3
注意Block1,2和3都立马开始运行,一个接一个。在Block0开始后,Block1等待了好一会儿才开始。同样,Block3在Block2之后才开始,但它先于Block2完成。
何时开始一个Block完全取决于GCD。如果一个Block的执行时间与另一个重叠,也是由GCD来决定是否将其运行在另一个不同的核心上,如果那个核心可用,否则就用上下文切换的方式来执行不同的Block。
有趣的是,GCD提供给你至少五个特定的队列,可根据队列类型选择使用。
Queue Types 队列类型
首先,系统提供给你一个叫做主队列(main queue)
的特殊队列。和其他串行队列一样,这个队列中得任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新UI的线程。这个队列就是用于发生消息给UIView
或发送通知的。系统同时提供给你好几个并发队列。它们叫做全局调度队列(Global Dispatch Queues)
。目前的四个全局队列有着不同的优先级:background
、low
、default
以及high
。要知道,Apple的API也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。
最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有五个队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。
以上是调度队列的大框架!
GCD的“艺术”归结为选择合适的队列来调度函数以提交你的工作。体验这一点的最好方式是走一遍下边的例子,我们沿途会提供一些一般性的建议。
入门
既然本教程的目标是优化且安全的使用GCD调用来自不同线程的代码,那么你将从一个近乎完成的叫做googlyPuff
的项目入手。
GooglyPuff是一个没有优化,线程不安全的应用,它使用Core Image 的人脸检测API来覆盖一对曲棍球眼睛到被检测到得人脸上。对于基本的图像,可以从相机胶卷选择,或用预设好的URL从互联网下载。
完成项目下载之后,将其解压到某个方便的目录,再用Xcode打开它并编译运行。这个应用看起来如下图所示:
p4
注意当你选择Le Internet
选项下载图片时,一个UIAlertView
过早地弹出。你将在本系列教程的第二部分修复这个问题。
这个项目中有四个有趣的类:
- PhotoCollectionViewController:他是应用开始的第一个视图控制器。它用缩略图展示所有选定的照片。
- PhotoDetailViewController:它执行添加曲棍球眼睛到图像上德逻辑,并用一个UIScrollView来显示结果图片。
- Photo:这是一个类,它根据一个
NSURL
的实例或一个ALAsset
的实例来实例化照片。这个类提供一个图像、缩略图以及从URL下载的状态。 - PhotoManager:它管理所有
Photo
的实例。
用dispatch_async处理后台任务
回到应用并从你的相机胶卷添加一些照片或使用Le Internet
选项下载一些。
注意在按下PhotoCollectionViewController
中得一个UICollectionViewCell
到生成一个新的PhotoDetailViewController
之间花了多久时间;你会注意到一个明显的滞后,特变是在比较慢得设备上查看很大的图。
在重载UIViewController的viewDidLoad
时容易加入太多杂波(too much clutter),这通常会引起视图控制器出现前更长的等待。如果可能,最好是卸下一些工作放到后台,如果他们不是绝对必须要运行在加载时间里。
这听起来像是dispatch_async
能做的事情!
打开PhotoDetailViewController
并用下面的实现替换viewDidLoad
:
- (void)viewDidLoad
{
[super viewDidLoad];
NSAssert(_image, @"Image not set; required to use view controller");
self.photoImageView.image = _image;
//Resize if neccessary to ensure it's not pixelated
if (_image.size.height <= self.photoImageView.bounds.size.height &&
_image.size.width <= self.photoImageView.bounds.size.width) {
[self.photoImageView setContentMode:UIViewContentModeCenter];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1
UIImage *overlayImage = [self faceOverlayImageFromImage:_image];
dispatch_async(dispatch_get_main_queue(), ^{ // 2
[self fadeInNewImage:overlayImage]; // 3
});
});
}
网友评论