iOS-你真的了解并发吗?

jopen 5年前

翻译了一篇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        }
</div>

每一个 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             })                      }
</div>

我们在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          })      }        }
</div>

你刚刚提交了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          })      }        }
</div>

需要注意的两个点:

  • 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  }
</div>
  • 3、可以取消特定的operation或者所有operation.operation添加到queue之后可以被取消, cancel() 方法被调用的时候取消已经完成。 当你取消任意一个operation,下面三个当中场景的其中一个会发生:

    • operation已经完成。这种情况cancel方法什么都不做。
    • operation正在执行.这种情况,系统不会强制停止operation,但会吧 cancelled 属性置为true
    • operation任然在等待执行。这种情况,operation将永远不会被执行。
    </li>
  • 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>

      现在让我们用NSOperationQueues重写刚刚那个的demo。首先声明属性,在ViewController里面:

      var queue = NSOperationQueue()
      </div>

      在 didClickOnstart() 方法里面修改代码:

       @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          })         }  }
      </div>

      使用 addOperationWithBlock 创建了一个operation,很简单是不?为了在主线程执行,替代了使用GCD中调用的 dispatch_async() ,我们可以用 NSOperationQueue.mainQueue() 实现相同的功能。

      上面的例子使用 addOperationWithBlock 来添加operation在queue。让我们来看看 NSBlockOperation 是如何实现的

      @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)  }
      </div>

      创建了一个 NSBlockOperation 实例把任务封装到block。 使用 NSBlockOperation ,允许设置完成的回调(completion handler). Operation完成后,回调会被调用。运行demo后,控制台上输出:

      Operation 1 completed  Operation 3 completed  Operation 2 completed  Operation 4 completed
      </div>

      取消Operations

      如上面提到的, NSBlockOperation 允许我们管理operations。现在我们创建一个cancel按钮放到导航栏,实现cancel这个事件。 为了说明cancel operation,添加依赖在 #2 和 #1 之间, 然后在添加 #3 和 #2 之间的依赖。 这就说明 #2 会在 #1 完成时候开始, #3 会在 #2 完成后开始。 #4 没有依赖会并发工作。

      取消所有operation的方法是 cancelAllOperations() ,在ViewController类里面添加。

      @IBAction func didClickOnCancel(sender: AnyObject) {                  self.queue.cancelAllOperations()     }
      </div>

      修改后的 didClickOnStart() 代码是

      @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
      </div>

      按下start按钮后点击取消按钮. 这些operation将被取消在operation #1 完成之后。

      在这里发生了什么?

      • 1、当 #1 已经执行,cancel不执行任何事情。第一张图片任然显示

      • 2、点击cancel按钮足够快, #2 被取消. cancelAllOperations() 阻止 #2 执行,所以image2没有显示

      • 3、 #3 依赖 #2 完成,因为 #2 取消了,所以 #2 也不能被执行

      • 4、 #4 没有依赖,并发执行,所以下载了图片.

      总结

      1、了解并发的概念、解释了GCD、创建串行和并发队列

      2、检验了NSOperationQueues的执行顺序

      3、学习了NSOperationQueues和GCD的优缺点

      参考

      完整的 Demo

      深入了解: https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html

      </div>

      来自: http://www.liuchendi.com/2016/01/05/iOS/29_GCD_Operation/