iOS-你真的了解并发吗?
翻译了一篇appcoda的文章,通俗易懂,理清了并发的相关知识点。
原文链接: http://www.appcoda.com/ios-concurrency/
并发一直被认为是iOS开发中的比较奇特的一部分。它被认为是危险的所以许多开发者尽可能避免使用。还有传言说尽可能避免使用多线程。如果你不能很好的理解多线程,多线程的确是危险的。猜想一下人们的生活中有多少危险的行为和活动,很多是吧?只有当我们掌握它,才能运用自如。并发是一个双刃剑,所以你必须掌握如何使用它。它帮助你写出高效、快速执行、响应迅速的app,但与此同时,如果误用则会毫不留情的毁坏你的app。这就是我们在写并发代码之前,首先考虑为什么需要并发,哪个API才能解决问题的原因。 在iOS里面我们有许多API可以使用,在这个教程里面,我们来谈谈最常用的 - NSOperation 和 Dispatch Queues
为什么需要并发?
我知道你是一个很棒的iOS开发者。无论你打算创造什么类型的app,你都需要知道并发能让你的app能够响应更迅速并且运行更快。这里总结了使用并发的几个优点
-
利用iOS设备硬件:现在全部iOS设备有多核处理器允许开发者并发执行多个任务。你应该利用这个特性发挥硬件的优势。
-
更好的体验:你也许曾经写过web服务、处理一些IO,或者运行一些繁重的任务。做这类型的操作在UI线程将堵塞app的运行。用户面对这种情况,直接关闭app。并发在处理这些任务的时候可以在后台运行,不会阻塞主线程,不会让用户感觉的厌烦,他们仍然可以点击按钮,滑动app里面的视图,所有繁重的任务都在后台运行了。
-
NSOperation和dispatch queues的使用让并发更加简单:创建和管理线程并不是一件简单的任务。这就是为什么大部分开发者惧怕听到并发和多线程。在iOS里面,并发的API使用起来非常简单。你不必要关心线程的创建或者是底层的管理。API会帮你做这些事。另外一个重要的优点非常容易实现同步避免资源竞争。竞争发生在多线程访问共享资源,这样会导无法预料的结果。通过同步,你保证了线程间的资源的共享。
关于并发你需要知道什么?
这这个教程里,我们将向你解释需要了解的并发知识,减轻你的恐惧。首先我们推荐先了解一些 blocks (Swift中的闭包),因为并发的API里面大量的使用这些知识。然后我们将谈谈 dispatch queues 和 NSOperationQueues ,我们将引导你了解并发的知识点、不同点、如何去使用。
Part 1: GCD (Grand Central Dispatch)
GCD 是最常用的API用来管理并发和执行异步操作在Unix底层系统中。GCD提供管理任务的队列。首先我们来看看队列(queue)是什么?
什么是Queues
队列是一个数据结构,管理对象的先进先出(FIFO)。 队列非常像电影院售票窗口排队情况。 谁先来舍就能买到票。在计算机科学中,第一个添加进队列的对象也是第一个被移除的。
Dispatch Queues
Dispatch queues 能够非常容易在你的应用中异步的并发执行任务。任务以 blocks 的形式提交到队列中。有两种类型的队列:
- (1) 串行队列(serial queues)
- (2) 并发队列(concurrent queues)
在谈他们之间的不同点时,你应该知道任务是不同线程中执行而不是单独的线程中处理。换句话说,你在主线程中创建的blocks提交了任务到dispatch queues.但是所有这些任务(Blocks of codes)将在不同的线程中执行。
Serial Queues
当你创建一个串行队列的时候,这个队列同一时间只能执行一个任务.所有在队列的任务都是平等的,按顺序执行。当然,你不必关心不同队列的任务,因为你仍然可以通过多个串行队列并发执行任务。举个例子:你可以创建两个串行队列,每个队列同一时刻执行一个任务,但是两个任务可以并发执行。
串行队列管理共享资源是非常有用的。他保证按顺序访问,防止竞争。想象一下只有售票口,但是一群人都想去买,所有的工作人员在这里是共享资源。如果工作人员不得不同时给人们提供服务,那么这将会变得没有秩序。为了解决这钟情况,需要要求人们排队(serial queue),所以工作人员才能同一时间为消费者提供服务。
另外一方面,这并不意味着电影院可以同一时间接纳一个消费者,假如它设定了多个售票口,他便可以为多个消费者服务。这就是我说的为什么使用多个串行队列任然可以执行多个任务。
使用串行队列的有点:
- 1、保证访问的有序性,防止资源的竞争。
- 2、任务有序的执行。提交到串行队列的任务将按顺序执行
- 3、可以创建多个串行队列
Concurrent Queues
顾名思义,并发队列允许并发的执行多个任务。每个任务(blocks of codes)按照他们添加到队列中的顺序启动。但是他们的执行是并发的,他们不必等其他任务开始。并发队列保证任务同一时间开始但不知道执行的顺序,执行时间、同一个时间点执行的任务数量。
举个列子,你提交了3个任务(#1, #2, #3)到一个并发队列.任务是并发执行的,启动的时候是按照添加到队列的顺序启动。然后,执行时间和完成时间是不一样的。有可能 #2 和 #3 开始花费了一些时间,也有可能在 #1 任务完成前启动。 由系统来决定执行这些任务。
使用Queues
刚刚我们解释了串行和并发队列,现在我们来看看怎么使用它们。默认情况下,系统提供给我们一个串行队列和4个并发队列。
main dispatch queue 是全局的串行队列在主线程中执行。用来更新APP UI和运行UIView相关的更新。同一时间只有一个任务被执行,这就是当我们运行繁重的任务时会阻塞UI
除了主队列,系统还提供4个并发队列。我们称他们是 Global Dispatch queues 。这些队列是全局的,通过优先级来区分。使用这些全局的并发队列,你可以获得首选队列使用 dispatch_get_global_queue ,其中第一个参数可以有下面几个值:
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
- DISPATCH_QUEUE_PRIORITY_BACKGROUND
这些队列类型代表着执行的优先级。 DISPATCH_QUEUE_PRIORITY_HIGH 优先级最高, DISPATCH_QUEUE_PRIORITY_BACKGROUND 优先级最低。所以你可以基于任务的优先级来使用这些队列。请注意,Apple的API也在使用这些队列,所以你的任务并不是唯一的在这些队列中。
最后,你可以创建任意数量的串行和并行队列。尽管你可以自己创建并发队列,但我强烈建议使用这4个全局的并发队列。
GCD 备忘录
现在,你已经基本了解了 dispatch queues ,我打算给你一个简单的备忘录,这个图非常简单,包含了所有你需要知道的GCD知识点。
很棒是吧?现在让我们来写一个demo来看看如何使用dispatch queues。我讲向你展示如何利用dispatch queues完善app的展示,让它更快的响应。
Demo Project
这个demo非常简单,展示4张图片,每一个需要请求网络。这个图片在主线程请求。为了展示给你UI是如何响应的,我添加了一个slider在image下面。 下载Demo
点击start按钮下载图片,与此同时拖动slider在下载期间,你会发现你不能拖动他。
当你点击开始按钮,图片开始被加载在主线程。很明显,这个方法非常糟糕,让UI没有反应。不幸的是现在还有好多APP在加载这样的任务的时候还放在主线程。现在我们用dispatch queues修改它。
首先我们将使用并发队列然后再使用串行队列
使用并发队列
打开 ViewController.swift ,在 didClickOnStart 方法是处理图片的下载。
@IBAction func didClickOnStart(sender: AnyObject) { let img1 = Downloader.downloadImageWithURL(imageURLs[0]) self.imageView1.image = img1 let img2 = Downloader.downloadImageWithURL(imageURLs[1]) self.imageView2.image = img2 let img3 = Downloader.downloadImageWithURL(imageURLs[2]) self.imageView3.image = img3 let img4 = Downloader.downloadImageWithURL(imageURLs[3]) self.imageView4.image = img4 }
每一个 downloader 被认为是一个任务,所有任务都在主线程执行。现在,让我们获得一个全局并发队列的引用,这个优先级的Default
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) dispatch_async(queue) { () -> Void in let img1 = Downloader.downloadImageWithURL(imageURLs[0]) dispatch_async(dispatch_get_main_queue(), { self.imageView1.image = img1 }) }
我们在block里面提交了一个任务下载第一张图片.当图片下载完成,我们提交另一个任务给主线程,使用这个下载好的image更新视图. 换句话说,我们把这些任务放到后台线程下载,然后在主线程执行UI相关的操作. didClickOnStart 修改后的代码:
@IBAction func didClickOnStart(sender: AnyObject) { let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) dispatch_async(queue) { () -> Void in let img1 = Downloader.downloadImageWithURL(imageURLs[0]) dispatch_async(dispatch_get_main_queue(), { self.imageView1.image = img1 }) } dispatch_async(queue) { () -> Void in let img2 = Downloader.downloadImageWithURL(imageURLs[1]) dispatch_async(dispatch_get_main_queue(), { self.imageView2.image = img2 }) } dispatch_async(queue) { () -> Void in let img3 = Downloader.downloadImageWithURL(imageURLs[2]) dispatch_async(dispatch_get_main_queue(), { self.imageView3.image = img3 }) } dispatch_async(queue) { () -> Void in let img4 = Downloader.downloadImageWithURL(imageURLs[3]) dispatch_async(dispatch_get_main_queue(), { self.imageView4.image = img4 }) } }
你刚刚提交了4个并发下载图片的任务在 default queue 。现在运行app,它将快速的响应,并且下载的过程中还能够拖动slider
使用串行队列
另外一种解决的办法是使用串行队列。回到刚刚的 didClickOnStart() 方法,现在我们将使用串行队列来下载图片. 当我们使用串行队列的时候,我们应该注意引用了那一条队列。每个app有一个默认的串行队列(就是main queue)。 所以,我们在使用串行队列的时候,必须创建一个新的队列,否则任务可能在更新UI的时候执行,这会带来很糟糕的用户体验。你可以使用 dispatch_queue_create 来创建一个新的队列,修改后的代码是这样的:
@IBAction func didClickOnStart(sender: AnyObject) { let serialQueue = dispatch_queue_create("com.appcoda.imagesQueue", DISPATCH_QUEUE_SERIAL) dispatch_async(serialQueue) { () -> Void in let img1 = Downloader .downloadImageWithURL(imageURLs[0]) dispatch_async(dispatch_get_main_queue(), { self.imageView1.image = img1 }) } dispatch_async(serialQueue) { () -> Void in let img2 = Downloader.downloadImageWithURL(imageURLs[1]) dispatch_async(dispatch_get_main_queue(), { self.imageView2.image = img2 }) } dispatch_async(serialQueue) { () -> Void in let img3 = Downloader.downloadImageWithURL(imageURLs[2]) dispatch_async(dispatch_get_main_queue(), { self.imageView3.image = img3 }) } dispatch_async(serialQueue) { () -> Void in let img4 = Downloader.downloadImageWithURL(imageURLs[3]) dispatch_async(dispatch_get_main_queue(), { self.imageView4.image = img4 }) } }
需要注意的两个点:
- 1、比起并发队列下载,串行队列花费长一点的时间下载图片。原因是因为我们同一时间只下载一张图片。每一个任务需要等待先前的任务完成,然后再执行
- 2、image是按顺序加载的,image1,image2,image3,image4。这是因为串行队列在同一时间只能执行一个任务
Part 2: Operation Queues
GCD的底层是基于C来实现并发的。 Operation queues ,是基于GCD的抽象。这意味着可以像GCD一样执行任务,但是有面向对象的风格。总之,Operation queues让开发者使用起来更简单。
不像 GCD , Operation queues 不遵循先进先出顺序。 Operation queues 和 dispatch queues 的不同点是:
-
1、不遵从FIFO(先进先出): 在 Operation queues 可以设置operation的优先级、添加operation间的依赖,这意味着可以定义operation的执行顺序,某些operation必须在其他opeartion完成之后执行。这就没有遵从先进先出的概念.
-
2、默认的,operation是并发的: 不能改变成串行类型。但是能够通过设置operation之间的依赖关系来实现串行的功能。
-
3、Operation Queues是 NSOperationQueue 的实例,任务封装在 NSOperation 实例当中
NSOperation
提交到Operation Queues的任务都是 NSOperation 实例。 我们在GCD中讨论过任务提交的形式都是通过block。 这里也可以这样做,只不过打包在 NSOperation 中. 你可以简单的认为 NSOperation 是工作的一个单元。
NSOperation 是一个抽象的class,不能直接使用,所以我们不得不使用使用它的子类.在iOS中提供了两个现有的 NSOperation 的子类,这些类可以直接使用,但你任然可以使用自己的 NSOperation 子类运行这些操作,这两个类是:
-
1、NSBlockOperation:使用这个类用block形式初始化operation. 这个operation自己可以包含多个block,当所有block执行完毕,这个operation则被认为是完成
-
2、NSInvocationOperation: 用invok一个selector的方式初始化一个operation
那么,NSOperation的有点是什么?
-
1、首先,他们支持依赖关系,通过使用 addDependency 方法.当你需要开始一个operation依赖于另外一个operation执行完成时候,会使用NSOperation。
-
2、可以改变执行的优先级,设置 queuePriority 属性为下面的值,高优先级的会先执行。
public enum NSOperationQueuePriority : Int { case VeryLow case Low case Normal case High case VeryHigh }
-
3、可以取消特定的operation或者所有operation.operation添加到queue之后可以被取消, cancel() 方法被调用的时候取消已经完成。 当你取消任意一个operation,下面三个当中场景的其中一个会发生:
- operation已经完成。这种情况cancel方法什么都不做。
- operation正在执行.这种情况,系统不会强制停止operation,但会吧 cancelled 属性置为true
- operation任然在等待执行。这种情况,operation将永远不会被执行。
-
4、NSOperation有3个有用的bool属性 finished 、 cancelled 、 ready .
- finished 在执行完成后设置为true
- cancelled 在operation被调用 cancel() 方法后设置为true
- ready 在operation即将执行的时候设置true </ul> </li>
- 5、任何一个 NSOperation 有一个完成后被调用的block,这个block将被调用,当finished设置为true的时候 </ul>
-
1、当 #1 已经执行,cancel不执行任何事情。第一张图片任然显示
-
2、点击cancel按钮足够快, #2 被取消. cancelAllOperations() 阻止 #2 执行,所以image2没有显示
-
3、 #3 依赖 #2 完成,因为 #2 取消了,所以 #2 也不能被执行
-
4、 #4 没有依赖,并发执行,所以下载了图片.
现在让我们用NSOperationQueues重写刚刚那个的demo。首先声明属性,在ViewController里面:
</div>var queue = NSOperationQueue()
在 didClickOnstart() 方法里面修改代码:
</div>@IBAction func didClickOnStart(sender: AnyObject) { queue = NSOperationQueue() queue.addOperationWithBlock { () -> Void in let img1 = Downloader.downloadImageWithURL(imageURLs[0]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView1.image = img1 }) } queue.addOperationWithBlock { () -> Void in let img2 = Downloader.downloadImageWithURL(imageURLs[1]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView2.image = img2 }) } queue.addOperationWithBlock { () -> Void in let img3 = Downloader.downloadImageWithURL(imageURLs[2]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView3.image = img3 }) } queue.addOperationWithBlock { () -> Void in let img4 = Downloader.downloadImageWithURL(imageURLs[3]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView4.image = img4 }) } }
使用 addOperationWithBlock 创建了一个operation,很简单是不?为了在主线程执行,替代了使用GCD中调用的 dispatch_async() ,我们可以用 NSOperationQueue.mainQueue() 实现相同的功能。
上面的例子使用 addOperationWithBlock 来添加operation在queue。让我们来看看 NSBlockOperation 是如何实现的
</div>@IBAction func didClickOnStart(sender: AnyObject) { queue = NSOperationQueue() let operation1 = NSBlockOperation(block: { let img1 = Downloader.downloadImageWithURL(imageURLs[0]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView1.image = img1 }) }) operation1.completionBlock = { print("Operation 1 completed") } queue.addOperation(operation1) let operation2 = NSBlockOperation(block: { let img2 = Downloader.downloadImageWithURL(imageURLs[1]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView2.image = img2 }) }) operation2.completionBlock = { print("Operation 2 completed") } queue.addOperation(operation2) let operation3 = NSBlockOperation(block: { let img3 = Downloader.downloadImageWithURL(imageURLs[2]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView3.image = img3 }) }) operation3.completionBlock = { print("Operation 3 completed") } queue.addOperation(operation3) let operation4 = NSBlockOperation(block: { let img4 = Downloader.downloadImageWithURL(imageURLs[3]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView4.image = img4 }) }) operation4.completionBlock = { print("Operation 4 completed") } queue.addOperation(operation4) }
创建了一个 NSBlockOperation 实例把任务封装到block。 使用 NSBlockOperation ,允许设置完成的回调(completion handler). Operation完成后,回调会被调用。运行demo后,控制台上输出:
</div>Operation 1 completed Operation 3 completed Operation 2 completed Operation 4 completed
取消Operations
如上面提到的, NSBlockOperation 允许我们管理operations。现在我们创建一个cancel按钮放到导航栏,实现cancel这个事件。 为了说明cancel operation,添加依赖在 #2 和 #1 之间, 然后在添加 #3 和 #2 之间的依赖。 这就说明 #2 会在 #1 完成时候开始, #3 会在 #2 完成后开始。 #4 没有依赖会并发工作。
取消所有operation的方法是 cancelAllOperations() ,在ViewController类里面添加。
</div>@IBAction func didClickOnCancel(sender: AnyObject) { self.queue.cancelAllOperations() }
修改后的 didClickOnStart() 代码是
</div>@IBAction func didClickOnStart(sender: AnyObject) { queue = NSOperationQueue() let operation1 = NSBlockOperation(block: { let img1 = Downloader.downloadImageWithURL(imageURLs[0]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView1.image = img1 }) }) operation1.completionBlock = { print("Operation 1 completed, cancelled:\(operation1.cancelled)") } queue.addOperation(operation1) let operation2 = NSBlockOperation(block: { let img2 = Downloader.downloadImageWithURL(imageURLs[1]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView2.image = img2 }) }) operation2.addDependency(operation1) operation2.completionBlock = { print("Operation 2 completed, cancelled:\(operation2.cancelled)") } queue.addOperation(operation2) let operation3 = NSBlockOperation(block: { let img3 = Downloader.downloadImageWithURL(imageURLs[2]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView3.image = img3 }) }) operation3.addDependency(operation2) operation3.completionBlock = { print("Operation 3 completed, cancelled:\(operation3.cancelled)") } queue.addOperation(operation3) let operation4 = NSBlockOperation(block: { let img4 = Downloader.downloadImageWithURL(imageURLs[3]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView4.image = img4 }) }) operation4.completionBlock = { print("Operation 4 completed, cancelled:\(operation4.cancelled)") } queue.addOperation(operation4) }
执行的结果:
</div>Operation 3 completed, cancelled:true Operation 2 completed, cancelled:true Operation 1 completed, cancelled:true Operation 4 completed, cancelled:true
按下start按钮后点击取消按钮. 这些operation将被取消在operation #1 完成之后。
在这里发生了什么?
总结
1、了解并发的概念、解释了GCD、创建串行和并发队列
2、检验了NSOperationQueues的执行顺序
3、学习了NSOperationQueues和GCD的优缺点
参考
完整的 Demo
</div>来自: http://www.liuchendi.com/2016/01/05/iOS/29_GCD_Operation/