经朋友推荐,遇到这么一篇介绍并发的好文章,我把它翻译过来给大家读一读。我非专业翻译人员,水平有限。差错在所难免,各位读者如果发现文内有误,欢迎留言斧正!
R0uter
文章译自 appcoda iOS Concurrency: Getting Started with NSOperation and Dispatch Queues 

在 iOS 开发当中,并发总是会被看成是怪物级别的东西。它是很多开发者尽可能去避免的危险区域。很多谣言都说你应该尽可能地避免在代码里写多线程。我觉得,如果你不是十分了解并发的话,它的确挺危险的。但这仅仅是因为未知而变得危险罢了。你想想看人的一生要做多少危险的行为或者说行动,很多对吧?但是一旦他们掌握了并发,并发就变得不再那么吓人了。并发是一把你应该学习如何去使用的双刃剑。它帮助你写出高效、快速和敏捷的应用,但同时,误用它也会无情地毁掉你的应用。这就是为什么在写并发代码之前,要先想想为什么你需要使用并发以及到底该用哪个API 来解决你问题?在 iOS 里我们有不同的 API 可用。这个教程里我们将会讨论两个最常用的 API —— NSOperation 以及 Dispatch Queues。

iOS并发特性
iOS并发特性

我们为什么需要并发?

我知道你是一个有经验的 iOS 开发者。无论你要创建何种应用,总之,你都会需要了解并发来让你的应用更加敏捷和快速。这里我总结了几点学习或者使用并发的好处:

  • 利用 iOS 设备的硬件: 现在所有的 iOS 设备都有允许开发者同时执行多任务的多核心处理器。你应该利用这个性能并且从硬件中获益。
  • 更好的用户体验: 你可能会写代码来调用 web 服务,处理一些 IO,或者执行任何重度的任务。如你所知,在 UI 线程里做这些任务会卡住应用,导致应用无响应。一点用户遇到这样的情况,他的第一反应一定是强行关闭你的应用。使用并发,所有的这些任务都可以在后台完成而不需要挂起主线程或者打扰你的用户。应用在后台处理重度加载任务的同时,他们仍旧可以点击按钮,滚动导航。
  • 像 NSOperation 和 dispatch Queues 这样的 API 让并发更易用: 创建和管理线程不是一个简单的任务。这就是为什么大部分的开发者害怕遭遇并发和多线程代码。在 iOS 里我们有非常简单易用的 API 来使用并发而不需要那么痛苦和崩溃。你不需要关心线程的创建或者管理人和低级的任务。API 就会帮你实现同步并且避免竞争问题。竞争问题会在多线程尝试访问共享资源时导致的奇怪结果。通过使用同步,你可以保护在线程之间共享的资源。

关于并发你需要了解什么?

在这个教程里,我们会向你解释关于并发你需要了解的一切并且释放所以你对它的恐惧。首先作为并发 API 里重度使用的内容,我们推荐去看一眼 blocks (Swift 里的闭包)。然后我们将会讨论 dispatch queues 和 NSOperationQueues 。我们将会带你了解并发里的每个概念,不同点以及如何去实现它们。

Part 1: GCD (全局中央调度)

GCD 是管理并发代码和在系统的 UNIX 层级执行异步任务最常用的 API。GCD 提供和管理任务队列。首先,我们来看看什么是队列。

什么是队列?

队列是以先入先出(FIFO)规则管理对象的数据结构。队列跟电影院买票窗口前排的队差不多。票以先到先得的规则出售。队列前边的人要比队列后边的人先买到票。电脑里的队列跟这个差不多是因为添加到队列里的第一个对象也是第一个从队列里移除的对象。

序列
序列
图片来自: FreeImages.com/Sigurd Decroos

Dispatch Queues

调度队列1是执行异步任务和在你的应用里并发的简单方法。它们是从你应用的 blocks (代码块)提交的任务队列。有两种调度队列:(1)串行队列2,&(2)并行队列3。在考虑不同点之前,你需要知道分配给这两个队列的任务都会分别在在线程里执行而不是创建它们的那个线程里。换句话说,你创建一块代码然后在主线程把它提交给调度序列。但所有这些任务(代码块)将会在不同的线程执行而不是主线程。

译注:

[1]调度队列:dispatch queues,对于非必要的情况下,我还是尽可能译为中文以便理解。
[2]串行队列:serial queues
[3]并行队列:concurrent queues

串行队列

当你选择以串行来创建队列时,队列只能一次执行一个任务。所有的在同一串行队列里的任务将会互相参照然后串行执行。总之,它们不关心其他队列里的任务也就是说你仍旧可以通过使用多个串行队列并发地执行任务。不如说,你可以创建两个串行队列,每个队列一次只执行一个任务但是两个队列仍然是同时执行的。

串行队列用来管理共享资源是很棒的。它对共享资源的连续访问提供了保障并且避免竞争问题。想象一下只有一个售票处但有一堆人想要买电影票,这里这个售票处就是一个共享资源。如果员工不能一次给所有人提供服务这里将变得一片混乱。要处理这样的情况,就要要求人们排队(串行队列),这样员工就能一次给一个客户提供服务了。

再说一次,这不意味着电影院一次只能给一个客户提供服务。如果它设立的两个或者更多的售票处,自然就能同时给更多的客户提供服务。这就是为什么我对你说仍旧能通过使用多个串行队列来同时执行多个任务。

使用串行队列的好处是:

  1. 保证了对共享资源的串行访问避免了竞争问题;
  2. 任务以可预测的顺序执行。当你把任务提交到调度序列,它们将会以插入的顺序执行;
  3. 你可以创建任意数量的串行队列。

并行队列

顾名思义,并行队列允许你同时执行多个任务。任务(代码块)以它们添加到队列的允许启动。但是它们的执行都是同时并发的并且它们不会等待其他任务才启动。并行队列保证任务以相同的顺序启动但你不会知道执行的顺序,执行时间或者在特定时间多少任务执行了。

比如说,你提交了三个任务(任务#1,#2和#3)到并行队列。任务是并发执行的并且以它们添加到队列的顺序启动。总之,执行时间和完成时间是变化的。就算任务#2和任务#3需要一些时间来启动,它们都可能在任务#1之前完成。这取决于系统来决定任务的执行。

使用队列

现在我们已经解释了串行和并行队列,是时候来看看我们要如何使用它们了。默认来说,系统给每个应用提供了一个串行队列和四个并发队列。主调度队列是全局可用的串行队列,它在应用的主线程执行任务。它是送来更新应用的 UI 和执行所有与更新 UIView 相关的任务。一次只能执行一个任务,这就是为什么 UI 会被你在主序列执行的重度任务锁阻塞。

除了主队列,系统还提供了四个并发队列。我们叫它们全局调度队列。这些队列是应用全局的并且只有优先级不同而已。要使用全局并行队列,你需要通过函数 dispatch_get_global_queue 获取你偏好的队列引用,它在第一个形式参数接收下面的值:

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

这些队列类型表示执行的优先级。HIGH 的队列有着最高的优先级然后BACKGROUND 有着最低的优先级。这样你就可以决定你的任务要用那个队列了。同时也要记住这些队列也被苹果的 API 使用,所以你的任务不是唯一存在这些队列里的任务。

最后,你可以创建任意数量的串行或者并行队列。要是并行队列,我强烈推荐使用四个全局队列之一,尽管你也可以自己创建。

GCD 小抄

现在你应该对调度队列有了一个基本的了解。我将会给你一个简单的小抄供你参考。这个小抄非常简单,它包含了你需要知道关于 GCD 的所有信息。

gcd小抄
gcd小抄

屌屌哒,对吧?我们来搞个简单的 demo 看看如何使用调度队列。我将会给你展示如何使用调度队列来优化应用的性能并且让它更加敏捷。

Demo 项目

我们的新手项目非常简单,我们现实四个图片视图,每个都需要从远程站点获取特定的图片。图片获取在主线程完成。为了给你展示这个是如何影响 UI 响应的,我得在图片下边添加一个简单的滑动条。现在下载然后运行这个新手项目。点击 Start 按钮来开始下载图片同时拖动滑动条。你会发现你根本拖动不了。

并发demo
并发demo

一旦你点击了开始按钮,图片就开始从主线程下载。显然,这导致了非常不好的结果使得 UI 无响应了。不幸的是知道今天还是有不少应用仍然像这样在主线程加载重度任务。现在我们要来使用调度队列修复它。

首先我们将会用并行队列实现解决办法然后再用串行队列。

使用并行调度队列

现在回到 Xcode 项目中的  ViewController.swift 文件。如果你自己读代码,你就会看到动作方法 didClickOnStart 。这个方法处理了图片下载。现在我们这样执行任务:


每一个下载器被看做一个任务,所有的任务现在都是在主队列执行的。现在我们获取四个全局并行队列之一,那个默认优先级的队列。


我们首先通过 dispatch_get_global_queue 获取默认并行队列的引用,然后在代码块里我们提交下载第一个图像的任务。一旦图像下载完成,我们提交一个任务到主队列来用下载的图像更新图像视图。换句话说,我们把图像下载任务放到了后台线程,但在主队列执行 UI 相关的更新。

如果你对剩下的图像也这么做,你的代码应该看起来像这样:

你刚刚提交了四个图像下载作为并行任务到默认队列里。现在编译并运行应用,它应该会运行的快了一些(如果你编译错误,检查代码确保和上边的一样)。注意在下载的同时,你应该也可以拖动滑动条了。

使用串行调度队列

另一种替代的解决办法是使用串行队列。现在,回到 ViewController.swift  文件里的那个相同的 didClickOnStart() 方法。这次我们将会使用串行队列来下载图片。当使用串行队列时,你需要高度注意你在引用的是那个串行队列。每个应用都有一个默认的串行队列,它实际上是 UI 的主队列。所以记住当使用串行队列时,你必须创建新的队列,否则你将会在应用用来更新 UI 时执行你的任务。这会导致错误和延迟破坏用户体验。你可以使用 dispatch_queue_create 函数来创建一个新队列然后和之前我们做的一样把所以的任务提交上去。在修改之后,代码看起来像这样:


如同我们看到的那样唯一与并行队列不同的是串行队列的创建。当你再次编译并运行应用,你会看到图像再次在后台下载,这样你就可以继续与 UI 交互了。

 但你需要注意两件事:
  1. 与并行队列的情况相比,这需要一点时间来下载图像。原因是我们一次只下载一个图像。每个任务会等待前边的任务完成才开始执行。
  2. 图像以 image1, image2, image3, 和 image4的次序加载。这是因为队列是串行队列它每次执行一个任务。

Part 2: Operation Queues

GCD 是一个低级 C API,它使开发者并发地执行任务。操作队列4,另一方面,是对队列模式的高级抽象,而且是建立在 GCD 之上的。这意味着你可以像 GCD 那样并发执行任务,却是以面向对象的风格。简单来说,操作队列让开发者更爽一些。

不同于 GCD,它们不遵守先入先出原则。这是操作队列与调度队列的不同点:

  1. 不遵守 FIFO:在操作队列里,你可以给操作设置操作优先级还可以在操作之间添加依赖这意味着你可以定义某些操作只会在另外的操作完成之后才执行。这是为什么它们不遵循先入先出;
  2. 默认来说,它们是并发执行的:你不能改变它的类型到串行队列,但你还是有一个变通的方法来在操作队列里顺序执行操作的,那就是在操作之间使用依赖;
  3. 操作队列是 NSOperationQueue  类的实例,它的任务封装在 NSOperation 类的实例里。

NSOperation

提交到操作队列的任务是以 NSOperation 实例的形式提交的。我们在 GCD 里讨论过的,任务以代码块的形式提交。这里也是一样不过应该捆绑到 NSOperation 实例里边。你可以简单地把 NSOperation  想象为工作的单元。

NSOperation 是一个抽象类它不能直接使用,所以你必须使用 NSOperation  子类。在 iOS SDK 里,我们有两个具体的 NSOperation  子类。这些类可以直接使用,但你同样可以子类 NSOperation  然后创建你自己的类来执行任务。我们能直接使用的那两个类是:

  1. NSBlockOperation – 使用这个类来用一个或者多个代码块初始化操作。操作自身能够包含不只一个代码块,当所有代码块执行完毕,操作就算是结束;
  2. NSInvocationOperation – 使用这个类来初始化在特定对象里调用 selector  的操作

那 NSOperation 的优势在哪里?

1)首先,它们支持在 NSOperation 类里通过方法 addDependency(op: NSOperation) 来设置依赖。当你需要启动依赖其他操作的操作时,你可能得使用 NSOperation ;

NSOperation-示例

2)其次,你可以通过设置优先级 queuePriority 为以下值来改变执行优先级:

具有高优先级的操作会最先执行。

3)你可以取消特定的操作或者给定队列里的所有操作。操作在添加队列里之后可以取消。取消可以通过调用 NSOperation 类里的 cancel() 方法完成。当你取消任何操作,以下三者之一会发生:

  • 你的操作已经执行完毕。这样的话,取消方法也就无效了;
  • 你的操作已经在执行中。这样的话,系统不会强制你的操作代码停止,而是设置 cancelled  属性为 true ;
  • 你的操作还在队列里等待执行。这样的话,你的的操作不会再被执行。

4) NSOperation  有三个有用的布尔属性,它们是 finished , cancelled , 和 ready 。 finished  会在操作执行完毕时设置为 true 。 cancelled  会在操作已被取消时设置为 true 。 ready 会在操作即将被执行时设置为 true 。

5)任何 NSOperation  都有一个选项来设置完成代码块,一旦任务完成就会被调用。 NSOperation 的 finished  属性一旦设置为 true  ,代码块就会被调用。

现在让我们重新我们的项目demo,这次我们将会使用 NSOperationQueues 。首先在 ViewController 类里做如下声明:


接下来,用下面的代码替换 didClickOnStart 方法,然后看看我们是如何在 NSOperationQueue 里执行操作的:


如你所见,你使用 addOperationWithBlock  方法来创建一个新操作带有给定闭包。这很简单不是吗?要在主队列执行任务而不是像我们使用 GCD 时的 dispatch_async() 我们可以在 NSOperationQueue 里做同样的事情( NSOperationQueue.mainQueue() )然后提交你想要在主队列里执行的操作。

你可以运行这个应用来快速测试一下。如果一切正常,应用应该能够在后台下载图像而不会卡住 UI。
在前面的栗子中,我们使用了 addOperationWithBlock 方法来给队列添加操作。让我们来看看如何使用 NSBlockOperation 做同样的事情,但是同时,还能给予我们功能和选项比如设置完成处理器。 didClickOnStart 方法被重写成这样:

我们给每个操作创建一个新的 NSBlockOperation 实例来封装任务到闭包。通过使用 NSBlockOperation ,你可以设置完成处理器。现在当操作结束,完成处理器将会被调用。简单来讲,我们只是记录了一个简单的信息来明确操作已经完成。如果你运行这个demo,你将会在终端里看到如下的输出:

取消操作

如同上边提到的, NSBlockOperation 允许你管理操作。现在让我们来看看如何取消操作。要这么做,首先添加一个 bar button item 到导航栏然后给它命名为 Cancel 。要演示取消操作,我们将会添加操作#2和操作#1之间的依赖,以及在操作#3和操作#2之间添加另外一个依赖。这意味着操作#3将会在操作#1完成后执行,操作#3将会在操作#2完成之后执行。操作#4没有依赖它会并发执行。要取消操作你所需要做的就是调用 NSOperationQueue 的 cancelAllOperations() 方法。在 ViewController 类里插入如下方法:


你住你需要把你添加的 Cancel  按钮关联到 didClickOnCancel 方法。你可以通过返回到 Main.storyboard 文件打开连接管理器。在那里你会在Received Actions里看到未连接的 didSelectCancel()  。点击 + 从空心圆拖动到 Cancel  按钮上。然后在 didClickOnStart 方法里创建依赖如下:


接下来改变操作#1的完成闭包来记录 cancelled  状态:


你可能需要为操作#2,#3和#4改变log文字,这样你会对过程有一个更好的认识。现在我们来编译和运行。在你点击了 Start  按钮后,点击 Cancel  按钮。这会取消操作#1完成后的所有操作。这里是要发生的事情:

    • 对操作#1来说已经执行了,取消也无济于事。这就是为什么 cancelled 值被记录为 false ,所以应用仍然显示了图像#1;
    • 如果你点 Cancel  按钮足够快,操作#2会被取消。 cancelAllOperations() 的调用会停止它的执行,所以图像#2没有下载;
    • 操作#3已经在队列里了,等待操作#2完成。它依赖操作#2的完成然而#3被取消了,操作#3将不会执行并被立即踢出队列;
    • 对于操作#4来说,没有任何依赖。所以它并发执行下载了#4。
ios-取消并发-demo
ios-取消并发-demo

如何深入?

在这个教程里,我带你入门了 iOS 并发的理论以及如何在 iOS 里实现它。我为你做了一个关于并发的很好的介绍,解释了 GCD,并且给你展示了如何创建串行和并行队列。另外,我们还了解了 NSOperationQueues 。你现在应该很清楚全局中心调度和 NSOperationQueue 之间的区别。

要进一步了解 iOS 并发,我建议你阅读苹果的并发指南

要是参考的话,你可以在 iOS Concurrency repository on Github 找到我们在这里提到的完整源代码。

文章译自 appcoda iOS Concurrency: Getting Started with NSOperation and Dispatch Queues

发布者:R0uter

如非声明,本人所著文章均为原创手打,转载请注明本页面链接和我的名字。

留下评论

电子邮件地址不会被公开。 必填项已用*标注