GCD

来源:互联网 发布:淘宝联盟订单失效 编辑:程序博客网 时间:2024/06/07 07:00

什么是GCD


更多有关单例线程安全等请移步 我是原文

GCD 是libdispatch 的市场名称,而 libdispatch 作为 Apple 的一个库,为并发代码在多核硬件(跑 iOS 或 OS X )上执行提供有力支持。它具有以下优点:

  • GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
  • GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
  • 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 会决定有多少并行的需求。并行要求并发,但并发并不能保证并行。

更深入的观点是并发实际上是关于构造。当你在脑海中用 GCD 编写代码,你组织你的代码来暴露能同时运行的多个工作片段,以及不能同时运行的那些。如果你想深入此主题,看看 这个由Rob Pike做的精彩的讲座 。

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) 。目前的四个全局队列有着不同的优先级:backgroundlowdefault 以及 high。要知道,Apple 的 API 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。

最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有五个队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。

以上是调度队列的大框架!

GCD 的“艺术”归结为选择合适的队列来调度函数以提交你的工作。

用 dispatch_async 处理后台任务


- (void)viewDidLoad{       [super viewDidLoad];    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1        代码块1(与UI无关)        dispatch_async(dispatch_get_main_queue(), ^{ // 2            代码块2(与UI有关) // 3        });    });}

下面来说明上面的新代码所做的事:

你首先将工作从主线程移到全局线程。因为这是一个 dispatch_async()Block 会被异步地提交,意味着调用线程地执行将会继续。这就使得viewDidLoad 更早地在主线程完成,让加载过程感觉起来更加快速。同时,一个人脸检测过程会启动并将在稍后完成。
在这里,代码块1完成耗时操作。接下来需要更新你的界面 ,那么你就添加一个新的 Block 到主线程。记住——你必须总是在主线程访问 UIKit 的类。
最后,代码块2更新 UI 。

正如之前提到的,dispatch_async添加一个 Block到队列就立即返回了。任务会在之后由 GCD 决定执行。当你需要在后台执行一个基于网络或 CPU 紧张的任务时就使用 dispatch_async ,这样就不会阻塞当前线程。

下面是一个关于在 dispatch_async 上如何以及何时使用不同的队列类型的快速指导:

  • 自定义串行队列:当你想串行执行后台任务并追踪它时就是一个好选择。这消除了资源争用,因为你知道一次只有一个任务在执行。注意若你需要来自某个方法的数据,你必须内联另一个 Block 来找回它或考虑使用 dispatch_sync
  • 主队列(串行):这是在一个并发队列上完成任务后更新 UI 的共同选择。要这样做,你将在一个 Block 内部编写另一个 Block 。以及,如果你在主队列调用 dispatch_async 到主队列,你能确保这个新任务将在当前方法完成后的某个时间执行。
  • 并发队列:这是在后台执行非 UI 工作的共同选择。

使用 dispatch_after 延后工作


稍微考虑一下应用的 UX 。是否用户第一次打开应用时会困惑于不知道做什么?你是这样吗? :]

如果用户的 PhotoManager 里还没有任何照片,那么显示一个提示会是个好主意!然而,你同样要考虑用户的眼睛会如何在主屏幕上浏览:如果你太快的显示一个提示,他们的眼睛还徘徊在视图的其它部分上,他们很可能会错过它。

显示提示之前延迟一秒钟就足够捕捉到用户的注意,他们此时已经第一次看过了应用。

- (void)showOrHideNavPrompt{    NSUInteger count = [[PhotoManager sharedManager] photos].count;    double delayInSeconds = 1.0;    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1     dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2         if (!count) {            [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"];        } else {            [self.navigationItem setPrompt:nil];        }    });}

showOrHideNavPromptviewDidLoad中执行,以及 界面被重新加载的任何时候。按照注释数字顺序看看:

你声明了一个变量指定要延迟的时长。
然后等待 delayInSeconds给定的时长,再异步地添加一个 Block 到主线程。
编译并运行应用。应该有一个轻微地延迟,这有助于抓住用户的注意力并展示所要做的事情。

dispatch_after工作起来就像一个延迟版的dispatch_async。你依然不能控制实际的执行时间,且一旦 dispatch_after 返回也就不能再取消它。

不知道何时适合使用dispatch_after

  • 自定义串行队列:在一个自定义串行队列上使用 dispatch_after 要小心。你最好坚持使用主队列。
  • 主队列(串行):是使用 dispatch_after 的好选择;Xcode 提供了一个不错的自动完成模版。
  • 并发队列:在并发队列上使用 dispatch_after 也要小心;你会这样做就比较罕见。还是在主队列做这些操作吧。

让你的单例线程安全


单例,不论喜欢还是讨厌,它们在 iOS 上的流行情况就像网上的猫。 :]

一个常见的担忧是它们常常不是线程安全的。这个担忧十分合理,基于它们的用途:单例常常被多个控制器同时访问。

单例的线程担忧范围从初始化开始,到信息的读和写。PhotoManager 类被实现为单例——它在目前的状态下就会被这些问题所困扰。要看看事情如何很快地失去控制,你将在单例实例上创建一个控制好的竞态条件。

找到 sharedManager ;它看起来如下:

+ (instancetype)sharedManager    {    static PhotoManager *sharedPhotoManager = nil;    if (!sharedPhotoManager) {        sharedPhotoManager = [[PhotoManager alloc] init];        sharedPhotoManager->_photosArray = [NSMutableArray array];    }    return sharedPhotoManager;}

当前状态下,代码相当简单;你创建了一个单例并初始化一个叫做photosArrayNSMutableArray属性。

然而,if 条件分支不是线程安全的;如果你多次调用这个方法,有一个可能性是在某个线程(就叫它线程A)上进入 if 语句块并可能在 sharedPhotoManager被分配内存前发生一个上下文切换。然后另一个线程(线程B)可能进入 if ,分配单例实例的内存,然后退出。

当系统上下文切换回线程A,你会分配另外一个单例实例的内存,然后退出。在那个时间点,你有了两个单例的实例——很明显这不是你想要的(译者注:这还能叫单例吗?)!

要强制这个(竞态)条件发生,替换 sharedManager为下面的实现:

+ (instancetype)sharedManager  {    static PhotoManager *sharedPhotoManager = nil;    if (!sharedPhotoManager) {        [NSThread sleepForTimeInterval:2];        sharedPhotoManager = [[PhotoManager alloc] init];        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);        [NSThread sleepForTimeInterval:2];        sharedPhotoManager->_photosArray = [NSMutableArray array];    }    return sharedPhotoManager;}

上面的代码中你用NSThreadsleepForTimeInterval:类方法来强制发生一个上下文切换。

打开AppDelegate.m并添加如下代码到 application:didFinishLaunchingWithOptions:的最开始处:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{    [PhotoManager sharedManager];});dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{    [PhotoManager sharedManager];});

这里创建了多个异步并发调用来实例化单例,然后引发上面描述的竞态条件。

编译并运行项目;查看控制台输出,你会看到多个单例被实例化,如下所示:

这里写图片描述

注意到这里有好几行显示着不同地址的单例实例。这明显违背了单例的目的,对吧?:]

这个输出向你展示了临界区被执行多次,而它只应该执行一次。现在,固然是你自己强制这样的状况发生,但你可以想像一下这个状况会怎样在无意间发生。

注意:基于其它你无法控制的系统事件,NSLog 的数量有时会显示多个。线程问题极其难以调试,因为它们往往难以重现。

要纠正这个状况,实例化代码应该只执行一次,并阻塞其它实例在 if 条件的临界区运行。这刚好就是 dispatch_once 能做的事。

在单例初始化方法中用dispatch_once取代 if 条件判断,如下所示:

+ (instancetype)sharedManager{    static PhotoManager *sharedPhotoManager = nil;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        [NSThread sleepForTimeInterval:2];        sharedPhotoManager = [[PhotoManager alloc] init];        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);        [NSThread sleepForTimeInterval:2];        sharedPhotoManager->_photosArray = [NSMutableArray array];    });    return sharedPhotoManager;}

编译并运行你的应用;查看控制台输出,你会看到有且仅有一个单例的实例——这就是你对单例的期望!:]

现在你已经明白了防止竞态条件的重要性,从 AppDelegate.m中移除 dispatch_async 语句,并用下面的实现替换 PhotoManager单例的初始化:

+ (instancetype)sharedManager{    static PhotoManager *sharedPhotoManager = nil;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        sharedPhotoManager = [[PhotoManager alloc] init];        sharedPhotoManager->_photosArray = [NSMutableArray array];    });    return sharedPhotoManager;}

dispatch_once()以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。

需要记住的是,这只是让访问共享实例线程安全。它绝对没有让类本身线程安全。类中可能还有其它竞态条件,例如任何操纵内部数据的情况。这些需要用其它方式来保证线程安全,例如同步访问数据,你将在下面几个小节看到。

1 0
原创粉丝点击