【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

本文章翻译自appcoda,作者:Gabriel Theodoropoulos

 多核多线程技术是*处理单元(CPU)自出现以来最大的技术改进之一,此技术的存在意味着CPU可以在任何时刻同时执行多个任务。

 串行任务执行或伪多任务执行已经存在多年,如果你经历过使用老式电脑的时代,或者如果你曾经有机会用旧操作系统的老式电脑,那么很容易理解我在说什么。但是无论CPU的内核数量多少,功能的强大程度如何,如果开发人员不利用这些可能性,这将变得毫无用处,因为才能让多任务和多线程编程发挥作用。开发人员可以且应该在任何设备上充分利用CPU的多任务功能,将程序可拆分的各个部分拆分到多个线程上并发执行。

 多核多线程开发的优点有很多,最重要的有:能尽快地执行优先度高的任务、给用户最好的体验、避免界面卡顿等等。试想看,假如某些应用程序在主线程上下载一大堆图像,下载完成前UI将完全无发响应任何操作。可想而知,用户将不会再想去使用这样的应用程序。

 在iOS中,苹果提供了两种方式去实现多任务处理:Grand Central Dispatch(GCD)和NSOperationQueue框架,这两者都能完美地分发任务于不同线程或者不同队列中。要使用哪一者取决于开发人员的决定,而这篇教程中我们将会把重点放在GCD的使用上。记住,多任务执行的使用有一条铁则:主线程应该永远用于界面展示和用户交互,任何耗时的或者高占用CPU的任务应该被放在并发队列或者后台队列中。这铁则对于新入门的开发人员来说或许难以消化且运用起来,但也必须要遵守它。

 Grand Central Dispatch最初是在iOS 4中引入的,它在尝试实现并发性、高性能和并行任务时提供了很大的灵活性和可定制性。在Swift 3之前,它有一个很大的缺点:晦涩的方法名让人难以记住和使用,因为它的编码风格与C相似,完全与Swift或者Objective-C的编码风格不同。这也是很多开发者故意避开GCD的主要原因,从而选择使用NSOperationQueue。网络上随意搜索有关于旧版本Swift中使用GCD的文章,都能让你更新UI感受到过去使用GCD的语法有多糟糕。

 然而在Swift 3中这一情况不再存在。GCD的使用方式是全新的,完全类似于Swift,新的语法使得开发人员更容易熟悉它。这些改变让我有动力写一篇关于Swift 3中GCD的最基本和最重要用法的文章。如果你使用的是旧的编码风格(甚至是一点点)的GCD,那么这个全新的语法将让你无法抗拒;如果不是,那么你也很快能驾轻就熟。

 在我们进入正文之前,我们先来谈一些关于GCD的概念。首先,GCD中的核心是dispatch queue。一个队列(queue)实际上是一个可以在主线程或后台线程上同步或异步执行的代码块。队列一旦被创建,操作系统便会接手对它的管理,并给它在CPU的任意内核上运行的时间。多队列也会有相应的管理,而这是开发人员不必处理的。队列之间是遵循FIFO(先进先出)模式的,这意味着首先被执行的队列代码也将首先完成(想象它像是在柜台前面排队的人,排前头的优先享受服务,排最尾的最后享受服务),我们后面会在例子中说明这一点。

 另一个重要的概念是work item。一个work item其实就是将在dispatch queue中运行的代码块,可以在创建队列的同时写入队列,也可单独创建后让队列调用且它可被多次使用(重用)。队列中work item的执行顺序也遵循FIFO模式。Work item的执行可以是同步的或异步的。在同步的情况下,正在运行的应用程序在完成work item的代码前不会退出该代码块。而异步情况下,正在运行的应用程序会在调用work item后立即返回。同样,我们将在后面例子中看到这些差异。

 上面所提到了(dispatch queue和work item),在此说明队列的串行(serial)和并发(concurrent)之分。在串行情况下,一个work item将在上一个work item执行完后开始执行(除非它是在队列中分配的第一个work item),而在并发情况下,不同work item将并行(*译者注:这里不应该是并行,而应该是并发具体可见并发与并行的区别)同时执行。

 当我们想要将任务分配到应用程序的主队列(main queue)时,必须保持谨慎。因为主线程应该始终保持可服务于用户的交互和用户界面的展示。说到这里,还有另一个铁则就是,当你想要作UI上的任何更新时必须始终在主线程上完成。如果你尝试在后台线程上进行UI更新,则无法保证是否会更新以及何时进行更新,并且很可能发生会让用户感受到不愉快的意外情况。但是,更新UI前所作的准备任务必须都已经完成,而这些任务完全允许后台进行。例如,你在次要的后台队列中下载图像的数据,但是你应该回到主线程上更新这些图像。

 需要记住的是,队列并不总是需要自己创建的。系统会创建全局调度队列(global dispatch queues),可被用于任何想要你想要运行的任务。关于队列所在的线程,iOS维护Apple称之为线程池(a pool of threads)中的线程,意思是除主线程以外的线程集合,并且系统会选择一个或多个线程来使用(取决于创建队列的多少以及创建它们的方式)。使用哪个线程是开发人员无法控制的,而是由操作系统根据其他并发任务的数量,处理器的负载等情况来“决定”,我相信也不会有谁想要去自己控制。

测试环境

 在这篇文章中,我们将使用小巧且具体的例子来阐述GCD的概念。通常情况下,我们不会去制作应用程序demo,而只用Xcode Playground进行工作,但是Playground中并不支持GCD的多线程使用所以,我们将通过建立一个工程项目来克服所有有可能出现的困难,你可以在这里下载到它

这几乎是一个空的工程,只做了以下两个添加:

  1. 在这个ViewController.swift文件中,你会发现一个定义但未实现的方法列表。在列表的每一个方法中,我们都会遇到GCD的一个新特性,而你需要做的就是在viewDidLoad(_:)方法取消注释其中一个从而调用它们。
  2. Main.storyboardViewController视图中你会找到已经添加好的imageView,并已通过IBOutletViewController类中属性产生关联稍后我们将通过这个imageView来演示实际开发中的案例。

下面将正式进入正文。

Dispatch Queues入门

 在Swift 3中,创建一个新的dispatch queue最简单方法如下:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 创建队列时你只需要给它赋予一个唯一的标签。使用逆序的DNS符号(“com.appcoda.myqueue”)是非常好的选择,因为它十分独特,这也是苹果推荐的做法。但这不是强制性的,只要保证标签唯一,使用任何字符串都是允许的。然而,队列的初始化方式不仅仅只有这一种;其实你可以在初始化时赋予更多的参数,我们会在后面谈论它。

 一旦队列被创建,我们就可以用它来执行代码,可以使用一个叫做sync的方法来同步执行,或者使用一个叫做async的方法来异步执行。因为我们刚开始入门,所以使用代码块(闭包)的方式来执行代码。稍后我们将初始化并使用派发任务项(DispatchWorkItem)对象而不是代码块(请注意:代码块在队列中也被视为任务项)。我们将从同步执行开始演示,该演示仅简单地打印数字0到9。

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

红点是为了轻易区分控制台中的打印结果,特别是当我们添加更多的队列或执行更多任务的时候。

 上面的for循环将在主队列上执行,而第一个循环将在后台运行。程序执行将在队列中停止; 它不会继续到主线程的循环,直到队列的任务完成,它不会显示从100到109的数字而这是因为我们做了一个同步执行。你也可以在控制台中看到

 将上面的代码段复制粘贴到上面提供的Starter Project下ViewController.swift文件的simpleQueues()方法中。确保此方法未在viewDidAppear(_:)方法中被注释,然后运行项目。注意Xcode控制台,你会发现没有发生什么奇特的事情,只会看到一些数字出现,而这并不能帮助我们得出有关GCD如何工作的结论。因此,你需要在simpleQueues()方法中后执行另外的代码块,它将执行打印100到109的数字(为了与上者区分):

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 这个for循环将在主队列上执行,而之前的循环将在后台运行。程序将会停留在queue.sync的代码块中执行任务,不会继续执行主线程中打印100到109数字的代码,除非queue.sync代码块中的任务完成。这是因为我们对queue调用了同步执行的方法,你可以在控制台中看到

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 那假如我们使用async方法从而让队列的代码块异步执行,会怎么样呢?其实在这种情况下,程序不会再等待队列任务的完成才执行后面的代码,而是会立即返回到主线程,第二个for循环将与“queue”中的循环同时执行。在我们看到打印前,请你使用以下async方法更新“queue”调用的方法:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 然后,运行并留意Xcode的控制台:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues


 同步执行相比,这种打印情况更为有趣; 
你会看到主队列(第二个
for循环)上的代码和我们的“queue”的代码并行运行。我们的自定义队列在开始的时候可以获得更多的运行时间,但这只是一个优先级问题(接下来我们会看到它)。在这里我们更重要的是要弄清楚:当我们有另一个任务在后台运行,而且该队列的任务项不是在主线程以同步执行时,我们的主队列是*“工作”的

 尽管上面的例子非常简单,但它清楚地体现了程序在同步队列和异步队列的运行行为不同。我们将在后面继续使用彩色的打印方式,要记住某个颜色是指在某个队列的任务执行结果,所以不同的颜色意味着不同的队列。

Quality Of Service (QoS) 和 Priorities

 在使用GCD和dispatch queue时,常常需要告诉系统:你的应用程序中哪些任务相对于其他任务更需要优先执行。当然,在主线程上运行的任务总是具有最高的优先级,因为主队列负责处理UI并时刻保持程序能响应操作。无论如何,通过向系统提供这些信息,iOS会保证列队以优先级顺序排列并提供所需的资源(如CPU的执行时间)。很自然,所有的任务最终都会得到完成,而区别在于哪些任务会更快完成,哪些更晚。

 任务的重要程度和优先级相关的信息称为GCD 服务质量(Quality of Service,简写为QoS)事实上,QoS是一组具备特定类型的enum,通过在队列初始化时提供适当的QoS值,可以指定所需的优先级。如果没有定义QoS,则队列会给定优先级的默认值。QoS可选值的文档可以在这里找到,请保证你会阅读该网页。以下列表总结了可用的QoS情况,也称为QoS类型。排第一意味着最高优先级,排最后是最低优先级:

  • userInteractive
  • userInitiated
  • default
  • utility
  • background
  • unspecified

 现在回到我们的项目中,我们将在queuesWithQoS()方法里进行操作。声明并初始化以下两个新的调度队列:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 可以注意到,我们给它们分配了相同的QoS类型,所以它们在执行期间具有相同的优先级。就像我们之前所做的那样,第一个队列将包含一个for循环以显示0到9的值(加上红点)在第二个队列中,我们将执行另一个for循环,它将显示从100到109的值(加上蓝点)。

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 我们现在看看,执行明知两个具有相同的优先级(相同QoS类型)队列的结果——不要忘了取消queuesWithQos()方法中queuesWithQos()的注释:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 通过查看上面的截图可以很容易地看出,这两个任务都是“均匀”执行的,实际上这是我们期望得到的结果。现在,让我们改变queue2的QoS类型为 utility(低优先级),如下所示:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 让我们看看会发生什么:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 毫无疑问,第一个调度队列(queue1)比第二个调度队列执行得更快,因为它的优先级更高。即使queue2在一开始获得执行的机会,系统还是将其资源主要提供给第一个队列,因为它被标记为更重要的一个。一旦第一个队列完成,系统才会关注第二个队列。

 我们来做另一个实验,这次我们将第一个队列的QoS类型改为background(后台)

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 这个优先级几乎是最低的,我们来看看在运行代码时会发生什么:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 这次第二个队列的完成执行更快,因为QoS类型中background比utility具有更高的优先级。

 以上内容让我们明确了QoS如何工作,但假如我们在主队列上执行任务会如何?让我们在方法的末尾添加下面的代码:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 结果如下:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 我们可以再次看到主队列默认具有高优先级,并且queue1队列与主队列并行执行。而queue2队列在其他两个队列的任务完成执行前没有得到很多的执行机会,是因为它的优先级最低。

Concurrent Queues(并发队列)

 到目前为止,我们已经看到dispatch queue是如何同步和异步地工作,以及Quality of Service各类型如何影响系统赋予队列的优先级。前面所有例子的共同点是,我们这些队列是串行(serial的。这意味着,如果我们将多个任务分配给其中某个队列,那么这些任务将一个接一个地按顺序执行,而不是同时一起执行。接下来,我们将看到如何让多个任务(工作项)同时运行,换句话说,我们将看到如何创建一个并发队列。

 进入我们的项目,来到concurrentQueues()方法(我们将取消viewDidAppear(_:)相应方法注释在这个新的方法中,让我们创建以下的新队列:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 现在,让我们将以下任务(或者称为工作项)分配给队列:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 当这段代码运行时,任务将以串行模式执行。这可在截图中很清楚看到:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 接下来,让我们修改anotherQueue队列的初始化

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 上述初始化有一个新的参数:attributes参数。该参数被赋予concurrent值时,队列的所有任务将被同时执行。如果你不指定这个参数,那么得到的会是一个串行(serial)队列。另外,QoS参数不是必需指定的的,我们可在这个初始化中省略它,也是不会有任何问题的。

 通过再次运行程序,我们注意到这些任务几乎是并行执行的:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 请注意,修改队列的QoS类型对任务的执行同样会有影响。但是,只要你将队列初始化为并发队列,那么这些任务的并行(译者注:此处应该是并发)执行就会受到尊重,并且都将得到运行时间。

 该attributes参数还可以接受另一个名为initiallyInactive的值。通过使用该值,任务的执行不会自动开始,而是必须由开发人员主动执行。在看到运行结果前,先要对代码进行一些修改。首先声明一个inactiveQueue属性,如下所示:是

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 现在让我们来初始化队列,并将其分配给inactiveQueue

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 在这种情况下使用成员属性是有必要的,因为在anotherQueue是在concurrentQueues()方法中定义的,作用域只在该方法之内,在退出该方法之后便无法再使用。

 此时再次运行程序,你可以看到控制台没有任何输出,这正如我们所料。我们可以通过在 viewDidAppear(_:) 方法中添加以下代码用以**队列:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 DispatchQueue 类activate()法会开始执行队列中任务。注意,这个队列并没有被指定为并行队列,因此任务会以串行的方式执行:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 此时有个问题,我们该如何初始化一个非**状态的并发队列呢?其实很简单,我们只需要使用一个包含两者的数组来替换传入attributes参数时的单一数值:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

延迟执行

 有时,程序运行过程中可能需要对工作项中block的执行进行延时操作。CGD允许你通过调用一个特殊的方法来完成这个任务,并且设定执行定义任务的时间。

 这一次,我们将在Stater Project项目中已经存在的queueWithDelay()方法中编写代码我们将从以下几行代码的添加开始:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 首先,我们像之前一样创建一个新的DispatchQueue对象,接下来我们将使用它。然后,我们打印当前date以便验证后面任务的等待时间,最后我们指定等待时间。延迟时间通常是一个DispatchTimeInterval枚举值(在内部被解析为一个整数值),把它添加到DispatchTime来指定延迟(接下来将会添加)。在上面的示例代码中,我们设置了两秒作为任务执行的等待时间。我们使用的seconds方法,但除此之外,还提供以下选择:

  • microseconds
  • milliseconds
  • nanoseconds

 现在,让我们使用这个队列:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 now()方法返回当前时间,并在该时间上加上我们想要的延迟时间。如果我们现在运行该应用程序,在控制台上能看:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 确实,DispatchQueue的任务在两秒钟后执行了。请注意,如果你不想使用上述任何预定义的方法,则可以直接将Double添加到当前时间:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 在这种情况下,任务将在当前时间0.75秒后执行。另外,你可以不用now()方法,但必须自行提供一个DispatchTime值作为参数。我在上面展示的是延迟执行队列中工作项的最简单方法,实际上也不需要任何其他的东西。

访问主队列和全局队列

 在前面的所有例子中,我们手动创建了我们要使用的调度队列。但是,并不总是需要这样做,特别是如果你不希望更改调度队列的属性。正如我在本文开头提到的,系统创建了一组后台调度队列,也称为全局队列(global queues),你可以像使用自定义队列一样*地使用它们,只要记住不要试图尽可能多地使用全局队列来滥用系统。
 在前面的所有例子中,我们手动创建了我们要使用的调度队列。但是,并不总是需要这样做,特别是如果你不希望更改调度队列的属性。正如我在本文开头提到的,系统创建了一组后台调度队列,也称为全局队列(global queues),你可以像使用自定义队列一样*地使用它们,只要记住不要试图尽可能多地使用全局队列来滥用系统。

 你可以像使用其他队列一样去使用它:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 使用全局队列时并没有太多的属性可以去修改,然而,你仍可以使用指定的QoS类别:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 如果你没有指定QoS类型(正如我们第一个例子),那么默认会使用default类型。

 无论使用全局队列与否, 需要经常访问主队列却是不可否认的事实,且最常用于UI的更新。从其他队列访问主队列很简单,如下一段代码所示,调用时指定是同步或异步执行:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 实际上,只需输入DispatchQueue.main.,就可以看到主队列的所有可用方法; Xcode会自动提供你可以在主队列中调用的所有方法,排上面是你常需要用的(实际上,这通常是适用的,在队列名称后面输入“.”符号,Xcode将自动建议任何队列的可用方法)。你也可以根据我们在前面的部分中看到的,为block的执行添加一个延迟。

 现在让我们通过模拟真实开发中的例子来学习如何通过主队列更新UI。在你的Starter Project项目Main.storyboard文件中ViewController场景包含一个imageView,并且相应的IBOutlet属性已连接到ViewController类。在这里,我们来到fetchImage()(仍然是空的)方法,我们将透过代码下载Appcoda的logo并将其显示在imageView上。下面的代码完成了我们想做的事情(在这一点上我不会讨论任何关于这个URLSession类以及它是如何使用的):

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 请注意,我们实际上并没有在主队列更新UI,而是试图在后台线程上执行dataTask(...)方法的completionHandler block现在编译并运行应用程序,看看会发生什么(不要忘记调用fetchImage()方法):

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 即使我们得到了图片下载完成的信息,但是我们无法在imageView中看到它,因为UI尚未更新。最有可能的是,图像会在初始消息后几分钟出现(如果在应用程序中执行其他任务时也不能保证会出现这种情况),但问题不仅仅如此;您还会得到一个很长的错误日志,抱怨UI的更新被放在了后台线程上进行。

 现在,让这个有问题的行为改为使用主队列来修改我们的UI。在编辑上面的方法时,只更改下面所示的部分,并注意主队列是如何使用的:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 再次运行该应用程序,会看到imageView在下载完成后立即获得image,主队列被真正调用并更新了我们的UI。

使用DispatchWorkItem对象

 Dispatchworkitem是一个代码块,可以供任何队列调用,因此其中代码可能运行在后台或者主线程。它十分简明,就像一堆你需要调用的代码,而不是像前面写入block那样编写代码。

  以下是work item最简单使用方法的说明:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 让我们通过一个小例子来了解DispatchWorkItem对象是如何使用的。来到useWorkItem()方法,首先添加以下代码

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 我们的work item的任务是将变量value自增5,我们将通过调用perform()方法来使用我们workItem

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

  这一行代码将会使work item在主线程中被调用,当然你也可以在其他的线程中调用,让我们来看下一个例子:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 这同样能完美调用。然而,有更快的方法来调用work item,dispatchqueue类提供了一个方便的方法:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 当一个work item被调用后,你可以通知你的主队列(或者其他你想同志的队列),如下所示:    

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 上面的代码是将变量的值打印在控制台上,而它是在work item被调用的时候执行的。现在将我们所学的全部运用到一起,以下是useWorkItem()方法的代码块:

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

 这是你运行程序后将得到的结果 (viewDidAppear(_:)中方法中已经调用了以上方法):

【翻译】Swift 3中的 Grand Central Dispatch(GCD)和Dispatch Queues

总结

 大多数时候,你在此文章中看到的已经足以应付多任务或并发编程了。然而, 要记住这篇文章并没有完全涉及到GCD的所有概念,或者有些概念虽然涉及到了,但是却没有太深入细节。我这么做的目的是想使文章简洁明了,让任何级别的开发人员都能读懂并且理解其中的内容。假如你平常不使用GCD,请认真考虑去尝试让繁重的任务从你的主队列中移出;假如有任务能被放在后台执行,请让把它们放在后台执行。无论如何,使用GCD并不是很难,而且只有通过使应用程序更快速响应,才能获得积极的结果。尽情享乐于GCD吧!

示例项目,你可以在GitHub上得到它。



著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。