ObjC/Swift 在 Swift 中应用 Grand Central Dispatch (上)

loveltyoic · 2015年01月28日 · 最后由 Kabie 回复于 2015年01月28日 · 7967 次阅读

本文译自Grand Central Dispatch Tutorial for Swift: Part 1/2

尽管 Grand Central Dispatch (GCD) 已经存在一段时间了,但并非每个人都知道怎么使用它。这是情有可原的,因为并发很棘手,而且 GCD 本身基于 C 的 API 在 Swift 世界中很刺眼。 在这两篇教程中,你会学到 GCD 的来龙去脉。第一部分解释了 GCD 可以做什么和几个基本功能。第二部分,你会学到一些 GCD 所提供的进阶功能。

起步

libdispatch是 Apple 所提供的在 IOS 和 OS X 上进行并发编程的库,而 GCD 正是它市场化的名字。GCD 有如下优点:

  • GCD 可以将计算复杂的任务放到后台执行,从而提升 app 的响应性能
  • GCD 提供了比锁和线程更简单的并发模型,帮助开发者避免并发的 bug。

为了理解 GCD,你需要了解一些线程和并发的概念。这些概念可能很含糊并且细微,所以先简要回顾一下。

串行 vs. 并发

这两个词用来描述任务的执行顺序。 串行 在同一时间点总是单独执行一个任务,而并发可以同时执行多个任务。

任务

在本教程中,你可以把任务当做一个闭包 (closure)。实际上,你可以将 GCD 和函数指针一起使用,但是一般很少这样使用。闭包更简单!

不记得 Swift 中的闭包?闭包是自含的,可保存传递并被调用的代码块。当调用的时候,他们的用法很像函数,可以有参数和返回值。除此之外,闭包可以“捕获”外部的变量,也就是说,它可以看到并记住它自身被定义时的作用域变量。

Swift 中的闭包和 OC 中的块 (block) 类似甚至于他们几乎就是可交换使用的。唯一的限制在于 OC 中不能使用 Swift 独有的特性,比如元组 (tuple)。但 OC 中的块可以安全的替换成 Swift 中的闭包。

同步 vs. 异步

这两个词描述的是函数何时将控制权返回给调用者,以及在返回时任务的完成情况。

同步函数只有在任务完成后才会返回。

异步函数会立即返回,不会等待任务完成。因此异步函数不会阻塞当前线程。

注意 -- 当你读到同步函数阻塞 (block) 当前进程或者函数是阻塞 (blocking) 函数时,不要困惑!动词阻塞(block)描述的是函数对当前线程的影响,和块 (block) 没有关系。同时记住 GCD 文档中有关 OC 的 block 可以跟 Swift 的闭包互换。

临界区(Critical Section)

这是一段不能并发执行的代码,也就是说两个线程不可以同时执行它。这通常是因为这段代码会修改共享的资源。否则,并发的进程同时修改同一个变量会导致错误。

竞态条件

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。竞态条件可能产生在代码检查时不易被发现的不可预期行为。

死锁

两个或更多的线程因等待彼此完成而陷入的困境称为死锁。第一个线程无法完成因为它在等待第二个线程完成。但是第二个线程也无法完成因为它在等待第一个线程完成。

线程安全

线程安全的代码是可以被多个线程或并发任务安全调用的,他不会造成任何问题(数据错误,崩溃等)。非线程安全的代码在同一时间只能单独执行。一段线程安全的代码如let a = ["thread-safe"]。由于数组是只读的,它可以被多个线程同时使用而不会引发问题。另一方面,var a = ["thread-unsafe"]是可变数组。这意味着它不是线程安全的,因为多个线程可以同时获取并修改这个数组,会得到不可预料的结果。非线程安全的变量和可变的数据结构在同一时刻应该只能被一个线程获取。

上下文切换

上下文切换是在进程中切换不同线程时保存和恢复程序执行状态的过程。这一过程在编写多任务 app 时相当常见,但是会造成一些额外开支。

并发 vs 并行

并发和并行经常会被同时提起,所以值得通过简短的解释来区分彼此。

并发代码中的单独部分可以同时执行。然而,这要由系统来决定并发怎样发生或是否发生。

多核设备通过并行来同时执行多个线程;然而,在单核设备中,必须要通过上下文切换来运行另一个线程或进程。这一过程通常发生的很快以至于给人并行的假象。如下图所示

尽管你可能在 GCD 之下编写并发执行的代码,但仍由 GCD 来决定并行的需求有多大。

深层次的观点是并发实际上是关乎结构的。当你编写 GCD 代码时,你组织你的代码来揭示出可以同时运行的工作,以及不可以同时运行的。如果你想深入了解这个主题,猛击Rob Pike

队列

GCD 提供了 调度队列 (dispatch queues)来处理提交的任务;这些队列管理着你向 GCD 提交的任务并且以先进先出(FIFO)的顺序来执行任务。这保证了第一个加入队列的任务第一个被执行,第二个加入的任务第二个开始执行,以此类推。

所有调度队列都是线程安全的从而让你可以同时在多个线程中使用它们。当你明白了调度队列如何为你的代码提供了线程安全性时,GCD 的优点就很明显了。关键是选择正确的调度队列种类和正确的 调度函数 (dispatching function)来提交你的任务。

顺序队列

顺序队列中的任务同一时间只执行一件任务,每件任务只有在先前的任务完成后才开始。同时,你并不知道一个任务完成到另一个任务开始之间的间隔时间,如下图所示:

任务的执行是在 GCD 掌控之下的;你唯一确定的就是 GCD 在同一时刻只执行一件任务并且按任务加入队列的顺序执行。

因为不会在顺序队列中同时执行两件任务,所以没有多个任务同时进入临界区的危险;这保证了临界区不会出现竞态条件。因此如果进入临界区的唯一途径就是通过向调度队列提交任务,那么可以保证临界区是安全的。

并发队列

并发队列中的任务可以保证按进入队列的顺序被执行...仅此而已!任务可能以任意顺序完成而且你不知道何时下一个任务会开始,或是任一时刻有多少任务在运行。再一次,这完全取决于 GCD。 下图展示了四个并发任务的例子:

任务 1,2 和 3 都运行的很快,一个接一个。但是任务 1 在任务 0 开始了一段时间后才开始。同时,任务 3 在任务 2 开始后才开始但是却更早完成。

何时开始一个任务完全取决于 GCD。如果一个任务的执行时间和另一个的发生重叠,将由 GCD 来决定是否要将任务运行在另一个可用的核上或是通过上下文切换来运行另一个程序。

有趣的是,GCD 为每种队列类型提供了至少5种特别的队列。

队列类型

首先,系统提供了一种特殊的顺序队列 main queue。和其他的顺序队列一样,在这个队列里的任务同一时刻只有一个在执行。然而,这个队列保证了所有任务会在主线程中执行,主线程是唯一一个允许更新 UI 的线程。这个队列用来向 UIView 对象发消息或发通知。

系统同时提供了几种并发队列。这些队列和它们自身的 QoS 等级相关。QoS 等级表示了提交任务的意图,使得 GCD 可以决定如何制定优先级。

  • QOS_CLASS_USER_INTERACTIVE: user interactive 等级表示任务需要被立即执行以提供好的用户体验。使用它来更新 UI,响应事件以及需要低延时的小工作量任务。这个等级的工作总量应该保持较小规模。
  • QOS_CLASS_USER_INITIATED: user initiated 等级表示任务由 UI 发起并且可以异步执行。它应该用在用户需要即时的结果同时又要求可以继续交互的任务。
  • QOS_CLASS_UTILITY: utility 等级表示需要长时间运行的任务,常常伴随有用户可见的进度指示器。使用它来做计算,I/O,网络,持续的数据填充等任务。这个等级被设计成节能的。
  • QOS_CLASS_BACKGROUND: background 等级表示那些用户不会察觉的任务。使用它来执行预加载,维护或是其它不需用户交互和对时间不敏感的任务。

要清楚 Apple 的 API 同时也使用了全局调度队列(global dispatch queue),所以你添加的任何任务都不是这些队列中的唯一任务。

最后,你可以创建自定义的顺序或并发队列。意味着你至少有5种队列:主队列(main queue),四种通用调度队列,加上任意你自己定制的队列!

以上就是调度队列的主要部分!

GCD 的“艺术”可归结为选择正确的队列调度函数来提交任务。最佳的学习方式就是通过下面的例子。

示例

因为这篇教程的目标是使用 GCD 优化程序以及在不同线程中安全的运行代码,所以你会以一个几近完成的项目 GooglyPuff 来开始。

GooglyPuff 是一个未优化,非线程安全的 app,使用 Core Image 的人脸识别 API 在人脸上叠加金鱼眼。初始图像可以从图片库中选择或是从网络下载一组预定的图片。

GooglyPuff_Swift_Start_1

一旦下载了工程,提取到合适的地方,打开 Xcode 并运行它。看起来如下:

注意到当你选择 Le Internet 选项来下载图片时,一个UIAlertController提示框会过早的弹出。你会在教程的第二部分修复这个问题。

这个工程中有 4 个需要关心的类:

  • PhotoCollectionViewController:app 启动后的第一个视图控制器。展示所有选择的图片的缩略图。
  • PhotoDetailViewController:为图片加上金鱼眼并在UIScrollView中展示。
  • Photo:描述图片属性的协议。提供图片,缩略图和状态。两个类实现了这个协议:DownloadPhotoNSURL实例化图片,AssetPhotoALAsset实例化图片。
  • PhotoManager:管理所有Photo对象。

使用dispatch_sync处理后台任务

返回 app 并从图片库中添加一些图片或使用 Le Internet 选项下载一些。

留意在轻触PhotoCollectionViewController中的UICollectionViewCell后要多久才能完成PhotoDetailViewController的初始化;此时存在明显的延迟,尤其是在较慢的设备上浏览较大的图片时。

一不小心就会在UIViewControllerviewDidLoad中填充过多杂乱的方法而造成超负荷;以至于经常要等待很久视图控制器才会出现。如果可能的话,最好将一些工作转移到后台去完成,如果这些工作在加载时不是必需的。

听起来是使用dispatch_async的时候!

打开PhotoDetailViewController然后用下面的实现替换viewDidload

override func viewDidLoad() {
  super.viewDidLoad()
  assert(image != nil, "Image not set; required to use view controller")
  photoImageView.image = image

  // Resize if neccessary to ensure it's not pixelated
  if image.size.height <= photoImageView.bounds.size.height &&
     image.size.width <= photoImageView.bounds.size.width {
    photoImageView.contentMode = .Center
  }

  dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) { // 1
    let overlayImage = self.faceOverlayImageFromImage(self.image)
    dispatch_async(dispatch_get_main_queue()) { // 2
      self.fadeInNewImage(overlayImage) // 3
    }
  }
}

上面代码的工作流程:

  1. 首先将工作从主线程上转移到全局队列中。因为这是一个dispatch_async调用,异步提交的闭包意味着调用线程会继续执行下去。这使得viewDidLoad在主线程上更早的完成从而让加载的过程在感觉上更迅速。同时,人脸识别过程已经开始并会在晚些时候完成。
  2. 在这时,人脸识别已经完成并生成一张新图片。因为要用这张新图片更新UIImageView,所以把一个闭包加入主线程中。记住 -- 必须总是在主线程中操作UIKit
  3. 最后,用fadeInNewImage更新 UI。

注意到你在使用 Swift 的尾随闭包(trailing closure)语法,将闭包写在参数括号的后面传给dispatch_async。这种语法看起来更清晰,因为闭包没有内嵌到函数括号中。

运行 app;选择一张图片然后你会明显地发现视图控制器载入更快了,随后金鱼眼会加入进来。这给 app 带来了很好的效果,因为你展示出图片修改前后的变化。同时,如果你试图加载一张极其巨大的图片,app 不会因为加载视图控制器而失去响应,这让 app 有很好的适应性。

正如前面所提到的,dispatch_async以闭包的形式向队列中追加了一项任务并立即返回了。这项任务会在 GCD 决定的稍后时间执行。当你需要执行网络请求或在后台执行繁重的 CPU 任务时,使用dispatch_async不会阻塞当前进程。

何时使用何种队列类型快速指南:

  • 自定义顺序队列:当你想顺序执行后台任务并追踪它时,这是一个很好的选择。因为同时只有一个任务在执行,因此消除了资源竞争。注意如果需要从方法中获取数据,你必须内置另一个闭包来得到它或者考虑使用dispatch_sync
  • 主队列(顺序):当并发队列中的任务完成需要更新 UI 的时候,这是一个通常的选择。为达此目的,需要在一个闭包中嵌入另一个闭包。同时,如果在主队列中调用dispatch_async来返回主队列,能保证新的任务会在当前方法完成后再执行。
  • 并发队列:通常用来执行与 UI 无关的后台任务。

获取全局队列的帮助变量(Helper Variable)

你可能注意到dispatch_get_global_queue的 QoS 等级参数写起来有些繁琐。这是由于qos_class_t被定义为一个结构体,它包含有Uint32型的属性value,而这个属性需要被转型为Int。在 Utils.swift 中添加一些全局的计算变量,使获取全局队列更方便一些:

var GlobalMainQueue: dispatch_queue_t {
  return dispatch_get_main_queue()
}

var GlobalUserInteractiveQueue: dispatch_queue_t {
  return dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.value), 0)
}

var GlobalUserInitiatedQueue: dispatch_queue_t {
  return dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)
}

var GlobalUtilityQueue: dispatch_queue_t {
  return dispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value), 0)
}

var GlobalBackgroundQueue: dispatch_queue_t {
  return dispatch_get_global_queue(Int(QOS_CLASS_BACKGROUND.value), 0)
}

回到 PhotoDetailViewController 中的viewDidLoad中,将dispatch_get_global_queuedispatch_get_main_queue替换为帮助变量:

dispatch_async(GlobalUserInitiatedQueue) {
  let overlayImage = self.faceOverlayImageFromImage(self.image)
  dispatch_async(GlobalMainQueue) {
    self.fadeInNewImage(overlayImage)
  }
}

这使得调度调用更易读并且很容易看出在使用哪个队列。

dispatch_after推迟任务

仔细思考你的 app 中的 UX。用户可能在第一次打开 app 的时候不知道该做什么,不是吗?

如果在 PhotoManager 类中没有图片的时候,给用户一个提示是个不错的主意。然而,你同时要考虑用户的视线怎样扫过屏幕:如果提示出现的太快,用户可能还在看其他的地方而忽略了提示。

推迟一秒钟再出现提示,此时便可抓住用户的注意力,因为他们已经对 app 有了第一印象。

将下面的代码加到showOrHideNavPrompt的实现中,它位于 PhotoCollectionViewController.swift 文件底部。

func showOrHideNavPrompt() {
  let delayInSeconds = 1.0
  let popTime = dispatch_time(DISPATCH_TIME_NOW,
                              Int64(delayInSeconds * Double(NSEC_PER_SEC))) // 1
  dispatch_after(popTime, GlobalMainQueue) { // 2
    let count = PhotoManager.sharedManager.photos.count
    if count > 0 {
      self.navigationItem.prompt = nil
    } else {
      self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
    }
  }
}

showOrHideNavPrompt会在viewDidLoad以及UICollectionView重新加载的时候被执行。代码解释如下:

  1. 声明推迟的时间。
  2. 等待delayInSeconds所表示的时间,然后将闭包异步地加入主队列中。

运行 app。在短暂的延迟后,提示会出现并吸引用户的注意。

dispatch_after的工作原理就像推迟的dispatch_async。一旦dispatch_after返回,你还是无法掌握实际的执行时间抑或是取消任务。

想知道何时使用dispatch_after

  • 自定义顺序队列:慎用。在自定义顺序队列中慎用dispatch_after。你最好留在主队列中。
  • 主队列(顺序):好主意。在主队列中使用dispatch_after是一个好主意;Xcode 对此有自动补全模板。
  • 并发队列:慎用。很少会这样使用,最好留在主队列中。

单例和线程安全

单例。爱也好,恨也罢,它们在 iOS 中就像猫之于互联网一样流行。:]

经常有人因为单例不是线程安全的而忧虑。这种担忧是很有道理的,考虑到他们的用法:单例经常被多个控制器同时使用。 PhotoManager 类是一个单例,所以你要仔细思考这个问题。

思考两种情形,初始化单例的过程和对他进行读写的过程。

先来看初始化。这看起来很简单,因为 Swift 在全局域中初始化变量。在 Swift 中,全局变量在首次使用时被初始化,并且保证初始化是原子操作。也就是说,初始化代码被视为临界区从而保证了初始化在其他线程使用全局变量之前就完成了。Swift 是怎么做到的?其实,Swift 在幕后使用了 GCD 中的dispatch_once,详见博客

dispatch_once以线程安全的方式执行且仅执行一次闭包。如果一个线程正处于临界区中 -- 被提交给dispatch_once的任务 -- 其他线程会阻塞直到它完成。并且一旦它完成,其他线程不会再执行临界区中的代码。用let将单例定义为全局常量,我们可以进一步保证变量在初始化后不会发生变化。从某种意义上说,所有 Swift 全局常亮量都天生是单例,并且线程安全地初始化。

但是我们仍需要考虑读和写。尽管 Swift 使用dispatch_once来确保单例初始化是线程安全的,但不能保证它所表示的数据类型也是线程安全的。例如用一个全局变量来声明一个类实例,但在类中还是会有修改类内部数据的临界区。此时就需要其他方式来达成线程安全,比如通过对数据的同步化使用 (synchronizing access)。

处理读写问题

实例化线程安全性不是单例的唯一问题。如果单例的属性表示一个可变对象,比如PhotoManager中的photos,那么你就需要考虑那个对象是否线程安全。

在 Swift 中任意用let声明的常量都是只读并且线程安全的。用var声明的变量是可变且非线程安全的,除非数据类型本身被设计成线程安全。Swift 中的集合类型比如ArrayDictionary,当声明为变量时不是线程安全的。那么像 Foundation 的容器NSArray呢?是线程安全的吗?答案是--“可能不是”!Apple 维护的一个帮助列表中有许多 Foundation 中非线程安全的类。

尽管很多线程可以同时读取一个Array的可变实例而不出问题,但如果一个线程在修改数组的同时另一个线程却在读取这个数组,这是不安全的。你的单例目前还不能阻止这种情况发生。

为了弄清楚问题,看看 PhotoManager.swift 中的addPhoto

func addPhoto(photo: Photo) {
  _photos.append(photo)
  dispatch_async(dispatch_get_main_queue()) {
    self.postContentAddedNotification()
  }
}

这是一个 方法,因为它修改了一个可变数组。

再看看photos属性:

private var _photos: [Photo] = []
var photos: [Photo] {
  return _photos
}

这个属性的 getter 方法是一个 方法。调用者得到一个数组的拷贝并且保护了原始数组不被改变,但是这不能保证一个线程在调用addPhoto来写的时候没有另一个线程同时也在调用 getter 方法读photos属性。

注意 :在上面的代码中,为什么调用者要获取photo数组的拷贝?在 Swift 中,参数或函数返回是通过值或引用来传递的。引用传递和 OC 中的传指针一样,这意味着你得到的是原始的对象,对这个对象的修改会影响到其他使用了这个对象引用的代码。值传递拷贝了对象本身,对拷贝的修改不会影响原始的对象。默认情况下,Swift 类实例是引用传递而结构体是值传递。

Swift 内置的数据类型,如ArrayDictionary,是用结构体来实现的,看起来传递集合类型会造成代码中出现大量的拷贝。不要因此担心内存使用问题。Swift 的集合类型经过优化,只有在需要的时候才进行拷贝,比如通过值传递的数组在第一次被修改的时候。

这是软件开发中经典的读者写者问题(Readers-Writers Problem)。GCD 使用 调度屏障 (dispatch barriers)提供了一个优雅的解决方案来生成读写锁

当跟并发队列一起工作时,调度屏障是一族行为像序列化瓶颈的函数。使用 GCD 的 barrier API 确保了提交的闭包是指定队列中在特定时段唯一在执行的一个。也就是说必须在所有先于调度屏障提交的任务已经完成的情况下,闭包才能开始执行。

当轮到闭包时,屏障执行这个闭包并确保队列在此过程不会执行其他任务。一旦闭包完成,队列返回到默认的执行方式。GCD 同时提供了同步和异步两种屏障函数。

下图说明了屏障函数应用于多个异步任务的效果:

注意队列开始就像普通的并发队列一样工作。但当屏障执行的时候,队列变成像顺序队列一样。就是说,屏障是唯一一个在执行的任务。在屏障完成后,队列恢复成普通的并发队列。

下面说明什么时候用 -- 什么时候不应该用 -- 屏障函数:

  • 自定义顺序队列:坏选择。因为顺序队列本身就是顺序执行,屏障不会起到任何帮助作用。
  • 全局并发队列:慎用。其他系统可能也在使用队列,你不应该出于自身目的而独占队列。
  • 自定义并发队列:最佳选择。用于原子操作或是临界区代码。任何需要线程安全的设置和初始化都可以使用屏障。

因为以上唯一合适的选择就是自定义并发队列,你需要生成一个这样的队列来处理屏障函数以隔离读写操作。并发队列允许多个线程同时的读操作。

打开 PhotoManager.swift 并在photos属性下面添加如下私有属性到类中:

private let concurrentPhotoQueue = dispatch_queue_create(
    "com.raywenderlich.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT)

使用dispatch_queue_create初始化一个并发队列concurrentPhotoQueue。第一个参数遵循反向 DNS 命名习惯;保证描述性以利于调试。第二个参数指出你的队列是顺序的还是并发的。

注意 :当在网上搜索例子时,你经常看到人们传0NULL作为dispatch_queue_create的第二个参数。这是一种过时的方法来生成顺序调度队列;最好用参数显示声明。

找到addPhoto并用如下实现替换之:

func addPhoto(photo: Photo) {
  dispatch_barrier_async(concurrentPhotoQueue) { // 1
    self._photos.append(photo) // 2
    dispatch_async(GlobalMainQueue) { // 3
      self.postContentAddedNotification()
    }
  }
}

来看这段代码如何工作的:

  1. 将写操作加入自定义的队列中。当临界区被执行时,这是队列中唯一一个在执行的任务。
  2. 将对象加入数组。因为是屏障闭包,这个闭包不会和concurrentPhotoQueue中的其他任务同时执行。
  3. 最终发送一个添加了图片的通知。这个通知应该在主线程中发送因为这涉及到 UI,所以这里分派另一个异步任务到主队列中。

这个任务解决了写问题,但是你还需要实现photos的读方法。

为确保和写操作保持线程安全,你需要在concurrentPhotoQueue中执行读操作。但是你需要从函数返回读数据,所以不能异步地提交读操作到队列里,因为异步任务不能保证在函数返回前执行。

因此,dispatch_sync是个极好的候选。

dispatch_sync同步提交任务并等到任务完成后才返回。使用dispatch_sync和调度屏障一起来跟踪任务;或是在需要等待返回数据时使用dispatch_sync

仍需小心。设想你调用dispatch_sync到当前队列中。这会造成死锁。因为调用在等待闭包完成,但是闭包无法完成(甚至根本没开始!),直到当前在执行的任务结束,但当前任务没法结束(因为阻塞的闭包还没完成)!这就要求你必须清醒的认识到你从哪个队列调用了闭包,以及你将任务提交到哪个队列。

概述一下何时何地使用dispatch_sync

  • 自定义顺序队列:非常小心;如果你在运行一个队列时调用dispatch_sync调度任务到同一个队列,你显然会制造死锁。
  • 主队列(顺序):非常小心,原理同上。
  • 并发队列:好选择。用在和调度屏障同步或是等待任务完成以继续后续处理。 还是在 PhotoManager.swift 中,替换photos如下: var photos: [Photo] { var photosCopy: [Photo]! dispatch_sync(concurrentPhotoQueue) { // 1 photosCopy = self._photos // 2 } return photosCopy } 分别来看每个号码注释:
  • 同步调度到concurrentPhotoQueue队列执行读操作。
  • 保存图片数组的拷贝到photoCopy并返回它。

恭喜 —— 你的PhotoManager单例已经是线程安全的了。不论你读或是写图片数组,你都有信心保证操作会安全的执行。

回顾

还是不能 100% 的确定 GCD 的本质?你可以自己创建使用 GCD 函数的简单例子,通过断点和NSLog来确保你明白发生了什么。

我这里有两张动态 GIF 图片来帮助你理解dispatch_asyncdispatch_sync。每张 GIF 上面都有代码辅助你理解;注意代码中的断点和相应的队列状态。

重访 dispatch_sync

override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_sync(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}

下面对图片中的几个状态做说明:

  1. 主队列按部就班的执行任务 —— 紧接着的任务是实例化包含viewDidLoadUIViewController类。
  2. viewDidLoad在主线程中执行。
  3. dispatch_sync闭包被加入到全局队列中稍后执行。主线程停下来等待闭包完成。同时,全局队列正在并发执行任务;记住闭包以 FIFO 的顺序从全局队列中取出,但是会并发地执行。全局队列首先处理dispatch_sync闭包加入前已经存在队列中的任务。
  4. 最后,轮到dispatch_sync闭包执行。
  5. 闭包执行完毕,主线程得以继续。
  6. viewDidLoad方法完成,主队列接着处理其它任务。

dispatch_sync把任务加入队列并一直等待其完成。dispatch_async做了差不多的工作,只是它不会等待任务完成,而是转而去继续其他工作。

重访 dispatch_async

override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_async(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}

  1. 主队列按部就班的执行任务 —— 紧接着的任务是实例化包含viewDidLoadUIViewController类。
  2. viewDidLoad在主线程中执行。
  3. dispatch_async闭包被加入到全局队列中稍后执行。
  4. viewDidLoaddispatch_async后继续向下执行,主线程继续其他任务。同时,全局队列正在并发执行任务;记住闭包以 FIFO 的顺序从全局队列中取出,但是会并发地执行。
  5. 执行dispatch_async所添加的闭包。
  6. dispatch_async闭包完成,NSLog输出到控制台。

在这个特别的例子中,第一个NSLog在第二个NSLog后执行。事实并非总是如此——这取决于硬件在彼时正在做什么,你无法控制或知晓哪个语句会先执行。“第一个”NSLog在某种调用情况下可能会先执行。

下一步?

在本教程中,你已经学到了如何编写线程安全的代码以及如何在保持主线程响应性的前提下执行 CPU 密集型的任务。

可以下载GooglyPuff,里面包含了本教程中所做的所有改进。教程的第二部分会在此基础上继续改进。

如果你打算优化自己的 app,你真的应该使用 Instruments 中的Time Profile 模板来测试。使用方法已经超出本教程范围,可以查看怎样使用 Instruments

同时确保你在真机上测试,因为在模拟器上测试会得到跟真实体验相差甚远的结果。

在教程的下篇你会更深入 GCD 的 API 中做些更酷的事情。

译者:loveltyoic

...看起来还挺复杂的。。。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号