做者:GABRIEL THEODOROPOULOS,原文连接,原文日期:2016-11-16
译者:小锅;校对:saitjr;定稿:CMBhtml
自中央处理器(CPU)出现以来,最大的技术进步当属多核处理器,这意味着它能够同时运行多条线程,而且能够在任什么时候刻处理至少一个任务。git
串行执行以及伪多线程都已经成为了历史,若是你经历过老式电脑的时代,又或者你接触过搭载着旧操做系统的旧电脑,你就能轻易明白个人话。可是,无论 CPU 拥有多少个核心,无论它有多么强大,开发者若是很差好利用这些优点 ,那就没有任何意义。这时就须要使用到多线程以及多任务编程了。开发者不只能够,并且必需要好好利用设备上 CPU 的多线程能力,这就须要开发者将程序分解为多个部分,并让它们在多个线程中并发执行。github
并发编程有不少好处,可是最明显的优点包括用更少的时间完成所需的任务,防止界面卡顿,展示更佳的用户体验,等等。想像一下,若是应用须要在主线程下载一堆图片,那种体验有多糟糕,界面会一直卡顿直到全部的下载任务完成;用户是绝对不接受这种应用的。编程
在 iOS 当中,苹果提供了两种方式进行多任务编程:Grand Central Dispatch (GCD) 和 NSOperationQueue。当咱们须要把任务分配到不一样的线程中,或者是非主队列的其它队列中时,这两种方法均可以很好地知足需求。选择哪种方法是很主观的行为,可是本教程只关注前一种,即 GCD。无论使用哪种方法,有一条规则必需要牢记:任何操做都不能堵塞主线程,必须使其用于界面响应以及用户交互。全部的耗时操做或者对 CPU 需求大的任务都要在并发或者后台队列中执行。对于新手来讲,理解和实践可能都会比较难,这也正是这篇文章的意义所在。swift
GCD 是在 iOS 4 中推出的,它为并发、性能以及并行任务提供了很大的灵活性和选择性。可是在 Swift 3 以前,它有一个很大的劣势:因为它的编程风格很接近底层的 C,与 Swift 的编程风格差异很大, API 很难记,即便是在 Objective-C 当中使用也很不方便。这就是不少开发都避免使用 GCD 而选择 NSOperationQueue 的主要缘由。简单地百度一下,你就能了解 GCD 曾经的语法是怎么样的。数组
Swift 3 中,这些都有了很大的变化。Swift 3 采用了全新的 Swift 语法风格改写了 GCD,这让开发均可以很轻松地上手。而这些变化让我有了动力来写这篇文章,这里主要介绍了 Swift 3 当中 GCD 最基础也最重要的知识。若是你曾经使用过旧语法风格的 GCD(即便只用过一点),那么这里介绍的新风格对你来讲就是小菜一碟;若是你以前没有使用过 GCD,那你就即将开启一段编程的新篇章。多线程
在正式开始讨论今天的主题前,咱们须要先了解一些更具体的概念。首先,GCD 中的核心词是 dispatch queue。一个队列实际上就是一系列的代码块,这些代码能够在主线程或后台线程中以同步或者异步的方式执行。一旦队列建立完成,操做系统就接管了这个队列,并将其分配到任意一个核心中进行处理。无论有多少个队列,它们都能被系统正确地管理,这些都不须要开发者进行手动管理。队列遵循 FIFO 模式(先进先出),这意味着先进队列的任务会先被执行(想像在柜台前排队的队伍,排在第一个的会首先被服务,排在最后的就会最后被服务)。咱们会在后面的第一个例子中更清楚地理解这个概念。闭包
接下来,另外一个重要的概念就是 WorkItem(任务项)。一个任务项就是一个代码块,它能够随同队列的建立一块儿被建立,也能够被封装起来,而后在以后的代码中进行复用。正如你所想,任务项的代码就是 dispatch queue 将会执行的代码。队列中的任务项也是遵循 FIFO 模式。这些执行能够是同步的,也能够是异步的。对于同步的状况下,应用会一直堵塞当前线程,直到这段代码执行完成。而当异步执行的时候,应用先执行任务项,不等待执行结束,当即返回。咱们会在后面的实例里看到它们的区别。并发
了解完这两个概念(队列和任务项)以后,咱们须要知道一个队列能够是串行或并行的。在串行队列中,一个任务项只有在前一个任务项完成后才能执行(除非它是第一个任务项),而在并行队列中,全部的任务项均可以并行执行。app
在为主队列添加任务时,不管什么时候都要加倍当心。这个队列要随时用于界面响应以及用户交互。而且记住一点,全部与用户界面相关的更新都必须在主线程执行。若是你尝试在后台线程更新 UI,系统并不保证这个更新什么时候会发生,大多数状况下,这会都用户带来很差的体验。可是,全部发生在界面更新前的任务均可以在后台线程执行。举例来讲,咱们能够在从队列,或者后台队列中下载图片数据,而后在主线程中更新对应的 image view。
咱们不必定须要每次都建立本身的队列。系统维护的全局队列能够用来执行任何咱们想执行的任务。至于队列在哪个线程运行,iOS 维护了一个线程池,即一系列除主线程以外的线程,系统会从中挑选一至多条线程来使用(取决于你所建立的队列的数据,以及队列建立的方式)。哪一条线程会被使用,对于开发者来讲是未知的,而是由系统根据当前的并发任务,处理器的负载等状况来进行“决定”。讲真,除了系统,谁又想去处理上述的这些工做呢。
在本文中,接下来咱们会使用几个小的,具体的示例来介绍 GCD 的概念。正常状况下,咱们使用 Playground 来演示就能够了,并不须要建立一个 demo 应用,可是咱们没办法使用 Playground 来演示 GCD 的示例。由于在 Playground 当中没法使用不一样的线程来调用函数,尽管咱们的一些示例是能够在上面运行的,但并非所有。所以,咱们使用一个正常的工程来进行演示,以克服全部可能碰到的潜在问题,你能够在这里下载项目并打开。
这个工程几乎是空的,除了下述额外的两点:
在 ViewController.swift
文件中,咱们能够看到一系列未实现的方法。每个方法中,咱们都将演示一个 GCD 的特性,你要作的事情就是在在 viewDidAppear(_:)
中去除相应方法调用的注释,让对应的方法被调用 。
在 Main.storyboard
中,ViewController
控制器添加了一个 imageView
,而且它的 IBOutlet 属性已经被正确地链接到 ViewController
类当中。稍后咱们将会使用这个 imageView
来演示一个真实的案例。
如今让咱们开始吧。
在 Swift 3 当中,建立一个 dispatch queue 的最简单方式以下:
let queue = DispatchQueue(label: "com.appcoda.myqueue")
你惟一要作的事就是为你的队列提供一个独一无二的标签(label)。使用一个反向的 DNS 符号("com.appcoda.myqueue")就很好,由于用它很容易创造一个独一无二的标签,甚至连苹果公司都是这样建议的。尽管如此,这并非强制性的,你可使用你喜欢的任何字符串,只要这个字符串是惟一的。除此以外,上面的构造方法并非建立队列的惟一方式。在初始化队列的时候能够提供更多的参数,咱们会在后面的篇幅中谈论到它。
一旦队列被建立后,咱们就可使用它来执行代码了,可使用 sync
方法来进行同步执行,或者使用 async
方法来进行异步执行。由于咱们刚开始,因此先使用代码块(一个闭包)来做为被执行的代码。在后面的篇幅中,咱们会初始化并使用 dispatch 任务项(DispatchWorkItem)来取代代码块(须要注意的是,对于队列来讲代码块也算是一个任务项)。咱们先从同步执行开始,下面要作的就是打印出数字 0~9 :
使用红点可让咱们更容易在控制台输出中识别出打印的内容,特别是当咱们后面添加更多的队列执行的时候
将上述代码段复制粘贴到 ViewController.swift
文件中的 simpleQueues()
方法内。确保这个方法在 ViewDidAppear(_:)
里没有被注释掉,而后执行。观察 Xcode 控制台,你会看到输出并无什么特别的。咱们看到控制台输出了一些数字,可是这些数字没有办法帮咱们作出关于 GCD 特性的任何结论。接下来,更新 simpleQueues()
方法内的代码,在为队列添加闭包的代码后面增长另外一段代码。这段代码用于输出数字 100 ~ 109(仅用于区别数字不一样):
for i in 100..<110 { print("Ⓜ️", i) }
上面的这个 for 循环会在主队列运行,而第一个会在后台线程运行。程序的运行会在队列的 block 停止,而且直到队列的任务结束前,它都不会执行主线程,也不会打印数字 100 ~ 109。程序会有这样的行为,是由于咱们使用了同步执行。你也能够在控制台中看到输出结果:
可是若是咱们使用 async
方法运行代码块会发生什么事呢?在这种状况下,程序不须要等待队列任务完成才往下执行,它会立马返回主线程,而后第二个 for 循环会与队列里的循环同时运行。在咱们看到会发生什么事以前,将队列的执行改用 async
方法:
如今,执行代码,并查看 Xcode 的控制台:
对比同步执行,此次的结果有趣多了。咱们看到主队列中的代码(第二个 for 循环)和 dispatch queue 里面的代码并行运行了。在这里,这个自定义队列在一开始的时候得到了更多的执行时间,可是这只是跟优先级有关(这咱们将在文章后面学习到)。这里想要强调的是,当另一个任务在后台执行的时候,主队列是处于空闲状态的,随时能够执行别的任务,而同步执行的队列是不会出现这种状况的。
尽管上面的示例很简单,但已经清楚地展现了一个程序在同步队列与异步队列中行为的差别。咱们将在接下来的示例中继续使用这种彩色的控制台输出,请记住,特定颜色代码特定队列的运行结果,不一样的颜色表明不一样的队列。
在使用 GCD 与 dispatch queue 时,咱们常常须要告诉系统,应用程序中的哪些任务比较重要,须要更高的优先级去执行。固然,因为主队列老是用来处理 UI 以及界面的响应,因此在主线程执行的任务永远都有最高的优先级。无论在哪一种状况下,只要告诉系统必要的信息,iOS 就会根据你的需求安排好队列的优先级以及它们所须要的资源(好比说所需的 CPU 执行时间)。虽然全部的任务最终都会完成,可是,重要的区别在于哪些任务更快完成,哪些任务完成得更晚。
用于指定任务重要程度以及优先级的信息,在 GCD 中被称为 Quality of Service(QoS)。事实上,QoS 是有几个特定值的枚举类型,咱们能够根据须要的优先级,使用合适的 QoS 值来初始化队列。若是没有指定 QoS,则队列会使用默认优先级进行初始化。要详细了解 QoS 可用的值,能够参考这个文档,请确保你仔细看过这个文档。下面的列表总结了 Qos 可用的值,它们也被称为 QoS classes。第一个 class 代码了最高的优先级,最后一个表明了最低的优先级:
userInteractive
userInitiated
default
utility
background
unspecified
如今回到咱们的项目中,此次咱们要使用 queueWithQos()
方法。先声明和初始化下面两个 dispatch queue:
let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated) let queue2 = DispatchQueue(label: "com.appcoda.queue2", qos: DispatchQoS.userInitiated)
注意,这里咱们使用了相同的 QoS class,因此这两个队列拥有相同的运行优先级。就像咱们以前所作的同样,第一个队列会执行一个循环并打印出 0 ~ 9(加上前面的红点)。第二个队列会执行另外一个打印出 100 ~ 109 的循环(使用蓝点)。
看到运行结果,咱们能够确认这两个队列确实拥有相同的优先级(相同的 QoS class)—— 不要忘记在 viewDidAppear(_:)
中关闭 queueWithQos()
方法的注释:
从上面的截图当中能够轻易看出这两个任务被“均匀”地执行,而这也是咱们预期的结果。如今让咱们把 queue2
的 QoS class 设置为 utility
(低优先级),以下所示:
let queue2 = DispatchQueue(label: "com.appcoda.queue2", qos: DispatchQoS.utility)
如今看看会发生什么:
毫无疑问地,第一个 dispatch queue(queue1)比第二个执行得更快,由于它的优先级比较高。即便 queue2
在第一个队列执行的时候也得到了执行的机会,但因为第一个队列的优先级比较高,因此系统把多数的资源都分配给了它,只有当它结束后,系统才会去关心第二个队列。
如今让咱们再作另一个试验,此次将第一个 queue 的 QoS class 设置为 background
:
let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.background)
这个优先级几乎是最低的,如今运行代码,看看会发生什么:
此次第二个队列完成得比较早,由于 utility
的优先级比较 background
来得高。
经过上述的例子,咱们已经清楚了 QoS 是如何运行的,可是若是咱们在同时在主队列执行任务的话会怎么样呢?如今在方法的末尾加入下列的代码:
for i in 1000..<1010 { print("Ⓜ️", i) }
同时,将第一个队列的 QoS class 设置为更高的优先级:
let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated)
下面是运行结果:
咱们再次看到了主队列默认拥有更高的优先级,queue1
与主列队是并行执行的。而 queue2
是最后完成的,而且妆其它两个队列在执行的时候,它没有获得太多执行的机会,由于它的优先级是最低的。
到目前为止,咱们已经看到了 dispatch queue 分别在同步与异步下的运行状况,以及操做系统如何根据 QoS class 来影响队列的优先级的。可是在前面的例子当中,咱们都是将队列设置为串行(serial)的。这意味着,若是咱们向队列中加入超过一个的任务,这些任务将会被一个接一个地依次执行,而非同时执行。接下来,咱们将学习如何使多个任务同时执行,换句话说,咱们将学习如何使用并行(concurrent)队列。
在项目中,此次咱们会使用 concurrentQueue()
方法(请在 viewDidAppear(_:)
方法中将对应的代码取消注释)。在这个新方法中,建立以下的新队列:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility)
如今,将以下的任务(或者对应的任务项)添加到队列中:
当这段代码执行的时候,这些任务会被以串行的方式执行。这能够在下面的截图上看得很清楚:
接下来,咱们修改下 anotherQueue
队列的初始化方式:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility, attributes: .concurrent)
在上面的初始化当中,有一个新的参数:attributes
。当这个参数被指定为 concurrent
时,该特定队列中的全部任务都会被同时执行。若是没有指定这个参数,则队列会被设置为串行队列。事实上,QoS 参数也不是必须的,在上面的初始化中,即便咱们将这些参数去掉也不会有任何问题。
如今从新运行代码,能够看到任务都被并行地执行了:
注意,改变 QoS class 也会影响程序的运行。可是,只要在初始化队列的时候指定了 concurrent
,这些任务就会以并行的方式运行,而且它们各自都会拥有运行时间。
这个 attributes
参数也能够接受另外一个名为 initiallyInactive
的值。若是使用这个值,任务不会被自动执行,而是须要开发者手动去触发。咱们接下来会进行说明,可是在这以前,须要对代码进行一些改动。首先,声明一个名为 inactiveQueue
的成员属性,以下所示:
var inactiveQueue: DispatchQueue!
如今,初始化队列,并将其赋值给 inactiveQueue
:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility, attributes: .initiallyInactive) inactiveQueue = anotherQueue
使用成员属性是有必要的,由于 anotherQueue
是在 concurrentQueues()
方法中定义的,只在该方法中可用。当它退出这个方法的时候,应用程序将没法使用这个变量,咱们也没法激活这个队列,最重要的是,可能会形成运行时崩溃。
如今从新运行程序,能够看到控制台没有任何的输出,这正是咱们预期的。如今能够在 viewDidAppear(_:)
方法中添加以下的代码:
if let queue = inactiveQueue { queue.activate() }
DispatchQueue
类的 activate()
方法会让任务开始执行。注意,这个队列并无被指定为并行队列,所以它们会以串行的方式执行:
如今的问题是,咱们如何在指定 initiallyInactive
的同时将队列指定为并行队列?其实很简单,咱们能够将两个值放入一个数组当中,做为 attributes
的参数,替代本来指定的单一数值:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .userInitiated, attributes: [.concurrent, .initiallyInactive])
有时候,程序须要对代码块里面的任务项进行延时操做。GCD 容许开发者经过调用一个方法来指定某个任务在延迟特定的时间后再执行。
此次咱们将代码写在 queueWithDelay()
方法内,这个方法也在初始项目中定义好了。咱们会从添加以下代码开始:
let delayQueue = DispatchQueue(label: "com.appcoda.delayqueue", qos: .userInitiated) print(Date()) let additionalTime: DispatchTimeInterval = .seconds(2)
一开始,咱们像一般同样建立了一个 DispatchQueue
,这个队列会在下一步中被使用到。接着,咱们打印了当前时间,以后这个时间将会被用来验证执行任务的延迟时间,最后咱们指定了延迟时间。延迟时间一般是一个 DispatchTimeInterval
类型的枚举值(在内部它被表示为整型值),这个值会被添加到 DispatchTime
中用于指定延迟时间。在这个示例中,设定的等待执行时间是两秒。这里咱们使用的是 seconds
方法,除此以外,还有如下的方法可使用:
microseconds
milliseconds
nanoseconds
如今开始使用这个队列:
delayQueue.asyncAfter(deadline: .now() + additionalTime) { print(Date()) }
now()
方法返回当前的时间,而后咱们额外把须要延迟的时间添加进来。如今运行程序,控制台将会打印出以下的输出:
的确,dispatch queue 中的任务在两秒后被执行了。除此以外,咱们还有别的方法能够用来指定执行时间。若是不想使用任务预约义的方法,你能够直接使用一个 Double
类型的值添加到当前时间上:
delayQueue.asyncAfter(deadline: .now() + 0.75) { print(Date()) }
在这个状况下,任务会被延迟 0.75 秒后执行。也能够不使用 now()
方法,这样一来,咱们就必须手动指定一个值做为 DispatchTime
的参数。上面演示的只是一个延迟执行的最简单方法,但实际上你也不大须要别的方法了。
在前面的全部例子当中,咱们都手动建立了要使用的 dispatch queue。实际上,咱们并不老是须要本身手动建立,特别是当咱们不须要改变队列的优先级的时候。就像我在文章一开头讲过的,操做系统会建立一个后台队列的集合,也被称为全局队列(global queue)。你能够像使用本身建立的队列同样来使用它们,只是要注意不能滥用。
访问全局队列十分简单:
let globalQueue = DispatchQueue.global()
能够像咱们以前使用过的队列同样来使用它:
当使用全局队列的时候,并无太多的属性可供咱们进行修改。可是,你仍然能够指定你想要使用队列的 Quality of Service:
let globalQueue = DispatchQueue.global(qos: .userInitiated)
若是没有指定 QoS class(就像本节的第一个示例),就会默认以 default
做为默认值。
不管你使不使用全局队列,你都不可避免地要常常访问主队列,大多数状况下是做为更新 UI 而使用。在其它队列中访问主队列的方法也很是简单,就以下面的代码片断所示,而且须要在调用的同时指定同步仍是异步执行:
DispatchQueue.main.async { // Do something }
事实上,你能够输入 DispatchQueue.main.
来查看主队列的全部可用选项,Xcode 会经过自动补全来显示主队列全部可用的方法,不过上面代码展现的就是咱们绝大多数时间会用到的(事实上,这个方法是通用的,对于全部队列,均可以经过输入 . 以后让 Xcode 来进行自动补全)。就像上一节所作的同样,你也能够为代码的执行增长延时。
如今让咱们来看一个真实的案例,演示如何经过主队列来更新 UI。在初始工程的 Main.storyboard
文件中有一个 ViewController
场景(sence),这个 ViewController
场景包含了一个 imageView
,而且这个 imageView 已经经过 IBOutlet
链接到对应的 ViewController
类文件中。在这里,咱们经过 fetchImage()
方法(目前是空的)来下载一个 Appcoda 的 logo 并将其展现到 imageView
当中。下面的代码完成了上述动做(我不会在这里针对 URLSession 作相关的讨论,以及介绍它如何使用):
func fetchImage() { let imageURL: URL = URL(string: "http://www.appcoda.com/wp-content/uploads/2015/12/blog-logo-dark-400.png")! (URLSession(configuration: URLSessionConfiguration.default)).dataTask(with: imageURL, completionHandler: { (imageData, response, error) in if let data = imageData { print("Did download image data") self.imageView.image = UIImage(data: data) } }).resume() }
注意,咱们并无在主队列更新 UI 界面,而是试图在 dataTask(...)
方法的 completion handler 里运行的后台线程来更新界面。编译、运行程序,看看会发生什么(不要忘记调用 fetchImage()
方法):
即便咱们获得了图片下载完成的信息,可是没有看到图片被显示到 imageView
上面,这是由于 UI 并无更新。大多数状况下,这个图片会在信息出现的一小会后显示出来(可是若是其余任务也在应用程序中执行,上述状况不保证会发生),不只如此,你还会在控制台看到关于在后台线程更新 UI 的一大串出错信息。
如今,让咱们改正这段有问题的行为,使用主队列来更新用户界面。在编辑上述方法的时候,只须要改动底下所示部分,并注意咱们是如何使用主队列的:
if let data = imageData { print("Did download image data") DispatchQueue.main.async { self.imageView.image = UIImage(data: data) } }
再次运行程序,会看到图片在下载完成后被正确地显示出来。主队列确实被调用并更新了 UI。
DispatchWorkItem
是一个代码块,它能够在任意一个队列上被调用,所以它里面的代码能够在后台运行,也能够在主线程运行。它的使用真的很简单,就是一堆能够直接调用的代码,而不用像以前同样每次都写一个代码块。
下面展现了使用任务项最简单的方法:
let workItem = DispatchWorkItem { // Do something }
如今让咱们经过一个小例子来看看 DispatchWorkItem
如何使用。前往 useWorkItem()
方法,并添加以下代码:
func useWorkItem() { var value = 10 let workItem = DispatchWorkItem { value += 5 } }
这个任务项的目的是将变量 value
的值增长 5。咱们使用任务项对象去调用 perform()
方法,以下所示:
workItem.perform()
这行代码会在主线程上面调用任务项,可是你也可使用其它队列来执行它。参考下面的示例:
let queue = DispatchQueue.global() queue.async { workItem.perform() }
这段代码也能够正常运行。可是,有一个更快地方法能够达到一样的效果。DispatchQueue
类为此目的提供了一个便利的方法:
queue.async(execute: workItem)
当一个任务项被调用后,你能够通知主队列(或者任何其它你想要的队列),以下所示:
workItem.notify(queue: DispatchQueue.main) { print("value = ", value) }
上面的代码会在控制台打印出 value
变量的值,而且它是在任务项被执行的时候打印的。如今将全部代码放到一块儿,userWorkItem()
方法内的代码以下所示:
func useWorkItem() { var value = 10 let workItem = DispatchWorkItem { value += 5 } workItem.perform() let queue = DispatchQueue.global(qos: .utility) queue.async(execute: workItem) workItem.notify(queue: DispatchQueue.main) { print("value = ", value) } }
下面是你运行程序后会看到的输出(记得在 viewDidAppear(_:)
方法中调用上面的方法):
这篇文章中提到的知识足够你应付大多数状况下的多任务和并发编程了。可是,请记住,还有其它咱们没有提到的 GCD 概念,或者文章有提到可是没有深刻讨论的概念。目的是想让本篇文章对全部层次的开发者都简单易读。若是你以前没有使用过 GCD,请认真考虑并尝试一下,让主队列从繁重的任务中解脱出来。若是有能够在后台线程执行的任务,让将其移到后台运行。在任何状况下,使用 GCD 都不困难,而且它能得到的正面结果就是让应用响应更快。开始享受 GCD 的乐趣吧!
能够在这个 Github 里找到本文使用的完整项目。
本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 http://swift.gg。