Grand Central Dispatch 基础教程:Part 1/2

原文 Grand Central Dispatch Tutorail for Swift: Part 1/2

原文作者:Bj1433384542976729.pngrn Olav Ruud

译者:Ethan Joe

尽管Grand Central Dispatch(以下简称为GCD)已推出一段时间了,但并不是所有人都明白其原理;当然这是可以理解的,毕竟程序的并发机制很繁琐,而且基于C的GCD的API对于Swift的新世界并不是特别友好。

在接下来的两节教程中,你将学习GCD的输入 (in)与输出 (out)。第一节将解释什么是GCD并了解几个GCD的基础函数。在第二节,你将学习几个更加进阶的GCD函数。

Getting Started

GCD是libdispatch的代名词,libdispatch代表着运行iOS与OS X的多核设备上执行并行代码的官方代码库。它经常有以下几个特点:

  • GCD通过将高代价任务推迟执行并调至后台运行的方式来提升App的交互速度。

  • GCD提供比锁与多线程更简单的并发模型,以此来避免一些由并发引起的Bug。

为了理解GCD,你需要明白一些与线程、并发的相关的概念。这些概念间有着细微且模糊的差别,所以在学习GCD前请简略地熟悉一下这些概念。

连续性 VS 并发性

这些术语用来描述一些被执行的任务彼此间的关系。连续性执行任务代表着同一时间内只执行一个任务,而并发性执行任务则代表着同一时间内可能会执行多个任务。

任务

在这篇教程中你可以把每个任务看成是一个闭包。 事实上,你也可以通过函数指针来使用GCD,但在大多数情况下这明显有些麻烦。所以,闭包用起来更简单。

不知道什么是Swift中的闭包?闭包是可被储存并传值的可调用代码块,当它被调用时可以像函数那样包含参数并返回值。

Swift中的闭包和Objective-C的块很相近,它们彼此间是可以相互交替的。这个过程中有一点你不能做的是:用Objective-C的块代码去交互具有Swift独有属性属性的闭包,比如说具有元组属性的闭包。但是从Swift端交互Objective-C端的代码则是毫无障碍的,所以无论何时你在文档中看到到的Objective-C的块代码都是可用Swift的闭包代替的。

同步 VS 异步

这些术语用来描述当一个函数的控制权返回给调用者时已完成的工作的数量。

同步函数只有在其命令的任务完成时才会返回值。

异步函数则不会等待其命令的任务完成,即会立即返回值。所以,异步函数不会锁住当前线程使其不能向队列中的下一位函数执行。

值得注意的是---当你看到一个同步函数锁住(block)了当前进程,或者一个函数是锁函数(blocking function)或是锁运算(block operation)时别认混了。这里的锁(blocks)是用来形容其对于自己线程的影响,它跟Objective-C中的块(block)是不一样的。再有一点要记住的就是在任何GCD文档中涉及到Objective-C的块代码都是可以用Swift的闭包来替换的。

临界区

这是一段不能被在两个线程中同时执行的代码。这是因为这段代码负责管理像变量这种若被并发进程使用便会更改的可共享资源。

资源竞争

这是一种软件系统在一种不被控制的模式下依靠于特定队列或者基于事件执行时间进行运行的情况,比如说程序当前多个任务执行的具体顺序。资源竞争可以产生一些不会在代码排错中立即找到的错误。

死锁

两个或两个以上的进程因等待彼此完成任务或因执行其他任务而停止当前进程运行的情况被称作为死锁。举个例子,进程A因等待进程B完成任务而停止运行,但进程B也在等待进程A完成任务而停止运行的僵持状态就是死锁。

线程安全性

具有线程安全性的代码可以在不产生任何问题(比如数据篡改、崩溃等)的情况下在多线程间或是并发任务间被安全的调用。不具有线程安全性的代码的正常运行只有在单一的环境下才可被保证。举个具有线性安全性的代码示例let a = ["thread-safe"]。你可以在多线程间,不产生任何bug的情况下调用这个具有只读性的数组。相反,通过var a = ["thread-unsafe"]声明的数组是可变可修改的。这就意味着这个数组在多线层间可被修改从而产生一些不可预测的问题,对于那些可变的变量与数据结构最好不要同时在多个线程间使用。

上下文切换

上下文切换是当你在一个进程中的多个不同线程间进行切换时的一种进程进行储存与恢复的状态。这种进程在写多任务App时相当常见,但这通常会产生额外的系统开销。

并发 VS 并行

并发和并行总是被同时提及,所以有必要解释一下两者间的区别。

并发代码中各个单独部分可以被"同时"执行。不管怎样,这都由系统决定以何种方式执行。具有多核处理器的设备通过并行的方式在同一时间内实现多线程间的工作;但是单核处理器设备只能在同一时间内运行在单一线程上,并利用上下文切换的方式切换至其他线程以达到跟并行相同的工作效果。如下图所示,单核处理器设备运行速度快到形成了一种并行的假象。

1.jpg

并发 VS 并行

尽管你会在GCD下写出使用多线程的代码,但这仍由GCD来决定是否会使用并发机制。并行机制包含着并发机制,但并发机制却不一定能保证并行机制的运行。

队列

GCD通过队列分配的方式来处理待执行的任务。这些队列管理着你提供给GCD待处理的任务并以FIFO的顺序进行处理。这就得以保证第一个加进队列的任务会被首个处理,第二个加进队列的任务则被其次处理,其后则以此类推。

连续队列

连续队列中的任务每次执行只一个,一个任务只有在其前面的任务执行完毕后才可开始运行。如下图所示,你不会知道前一个任务结束到下一个任务开始时的时间间隔。

2.jpg

连续队列

每一个任务的执行时间都是由GCD控制的;唯一一件你可以确保的事便是GCD会在同一时间内按照任务加进队列的顺序执行一个任务。

因为在连续队列中不允许多个任务同时运行,这就减少了同时访问临界区的风险;这种机制在多任务的资源竞争的过程中保护了临界区。假如分配任务至分发队列是访问临界区的唯一方式,那这就保证了的临界区的安全。

并发队列

并发队列中的任务依旧以FIFO顺序开始执行。。。但你能知道的也就这么多了!任务间可以以任何顺序结束,你不会知道下一个任务开始的时间也不会知道一段时间内正在运行任务的数量。因为,这一切都是由GCD控制的。

如下图所示,在GCD控制下的四个并发任务:

3.jpg

并发队列

需要注意的是,在任务0开始执行后花了一段时间后任务1才开始执行,但任务1、2、3便一个接一个地快速运行起来。再有,即便任务3在任务2开始执行后才开始执行,但任务3却更早地结束执行。

任务的开始执行的时间完全由GCD决定。假如一个任务与另一个任务的执行时间相互重叠,便由GCD决定(在多核非繁忙可用的情况下)是否利用不同的处理器运行或是利用上下文切换的方式运行不同的任务。

为了用起来有趣一些,GCD提供了至少五种特别的队列来对应不同情况。

队列种类

首先,系统提供了一个名为主队列(main queue)的特殊连续队列。像其他连续队列一样,这个队列在同一间内只能执行一个任务。不管怎样,这保证了所有任务都将被这个唯一被允许刷新UI的线程所执行。它也是唯一一个用作向UIView对象发送信息或推送监听(Notification)。

GCD也提供了其他几个并发队列。这几个队列都与自己的QoS (Quality of Service)类所关联。Qos代表着待处理任务的执行意图,GCD会根据待处理任务的执行意图来决定最优化的执行优先权。

  • QOS_CLASS_USER_INTERACTIVE: user interactive类代表着为了提供良好的用户体验而需要被立即执行的任务。它经常用来刷新UI、处理一些要求低延迟的加载工作。在App运行的期间,这个类中的工作完成总量应该很小。

  • QOS_CLASS_USER_INITIATED:user initiated类代表着从UI端初始化并可异步运行的任务。它在用户等待及时反馈时和涉及继续运行用户交互的任务时被使用。

  • QOS_CLASS_UTILITY:utility类代表着长时间运行的任务,尤其是那种用户可见的进度条。它经常用来处理计算、I/O、网络通信、持续数据反馈及相似的任务。这个类被设计得具有高效率处理能力。

  • QOS_CLASS_BACKBROUND:background类代表着那些用户并不需要立即知晓的任务。它经常用来完成预处理、维护及一些不需要用户交互的、对完成时间并无太高要求的任务。

要知道苹果的API也会使用这些全局分配队列,所以你分派的任务不会是队列中的唯一一个。

最后,你也可以自己写一个连续队列或是并发队列。算起来你起码最少会有五个队列:主队列、四个全局队列再加上你自己的队列。

以上便是分配队列的全体成员。

GCD的关键在于选择正确的分发函数以此把你的任务分发至队列。理解这些东西的最好办法就是完善下面的Sample Project。

Sample Project

既然这篇教程的目的在于通过使用GCD在不同的线程间安全地调用代码,那么接下来的任务便是完成这个名为GooglyPuff的半成品。

GooglyPuff是一款通过CoreImage脸部识别API在照片中人脸的双眼的位置上贴上咕噜式的大眼睛且线程不安全的App。你既可以从Photo Library中选择照片,也可以通过网络从事先设置好的地址下载照片。

GooglyPuff Swift Start 1

将工程下载至本地后用Xcode打开并编译运行。它看起来是这样的:

4.jpg

GooglyPuff

在工程中共有四个类文件:

  • PhotoCollectionViewController:这是App运行后显示的首个界面。它将显示所有被选照片的缩略图。

  • PhotoDetailViewController:它将处理将咕噜眼添加至照片的工作并将处理完毕的照片显示在UIScrollView中。

  • Photo:一个包含着照片基本属性的协议,其中有image(未处理照片)、thumbnail(裁减后的照片)及status(照片可否使用状态);两个用来实现协议的类,DownloadPhoto将从一个NSURL实例中实例化照片,而AssetPhoto则从一个ALAsset实例中实例化照片。

  • PhotoManager:这个类将管理所有Photo类型对象。

使用dispatch_async处理后台任务

回到刚才运行的App后,通过自己的Photo Library添加照片或是使用Le internet下载一些照片。

需要注意的是当你点击PhotoCollectionViewController中的一个UICollectionViewCell后,界面切换至一个新的PhotoDetailViewController所用的时间;对于那些处理速度较慢的设备来说,处理一张较大的照片会产生一个非常明显的延迟。

这种情况下很容易使UIViewController的viewDidLoad因处理过于混杂的工作而负载;这么做的结果便在view controller出现前产生较长的延迟。假如可能的话,我们最好将某些工作放置后台处理。

这听起来dispatch_async该上场了。

打开PhotoDetailViewController后将viewDidLoad函数替换成下述代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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. 你首先将照片处理工作从主线程(main thread)移至一个全局队列(global queue)。因为这是一个异步派发(dispatch_async的调用,闭包以异步的形式进行传输意味着调用的线程将会被继续执行。这样一来便会使viewDidLoad更早的在主线程上结束执行并使得整个加载过程更加流畅。与此同时,脸部识别的过程已经开始并在一段时间后结束。

  2. 这时脸部识别的过程已经结束并生成了一张新照片。当你想用这张新照片来刷新你的UIImageView时,你可以向主线程添加一个新的闭包。需要注意的是--主线程只能用来访问UIKit。

  3. 最后,你便用这张有着咕噜眼的fadeInNewImage照片来刷新UI。

有没有注意到你已经用了Swift的尾随闭包语法(trailing closure syntax),就是以在包含着特定分配队列参数的括号后书写表达式的形式了向dispatch_async传递闭包。假如把闭包写出函数括号的话,语法会看起来更加简洁。

运行并编译App;选一张照片后你会发现view controller加载得很快,咕噜眼会在很短的延迟后出现。现在的运行效果看起来比之前的好多了。当你尝试加载一张大得离谱的照片时,App并不会在view controller加载时而延迟,这种机制便会使App表现得更加良好。

综上所述,dispatch_async将任务以闭包的形式添加至队列后立即返回。这个任务在之后的某个时间段由GCD所执行。当你要在不影响当前线程工作的前提下将基于网络或高密度CPU处理的任务移至后台处理时,dispatch_asnyc便派上用场了。

接下来是一个关于在使用dispatch_asnyc的前提下,如何使用以及何时使用不同类型队列的简洁指南:

  • 自定义连续队列(Custom Serial Queue): 在当你想将任务移至后台继续工作并且时刻监测它的情况下,这是一个不错的选择。需要注意的是当你想从一个方法中调用数据时,你必须再添加一个闭包来回调数据或者考虑使用dispatch_sync。

  • 主队列(Main Queue[Serial]):这是一个当并发队列中的任务完成工作时来刷新UI的普遍选择。为此你得在一个闭包中写入另一个闭包。当然,假如你已经在主线程并调用一个面向主线程的dispatch_async的话,你需要保证这个新任务在当前函数运行结束后的某个时间点开始执行。

  • 并发队列(Concurrent Queue):对于要运行后台的非UI工作是个普遍的选择。

获取全局队列的简洁化变量

你也许注意到了dispatch_get_global_queue函数里的QoS类的参数写起来有些麻烦。这是因为qos_class_t被定义成一个值类型为UInt32且最后还要被转型为Int的结构体。我们可以在Utils.swift中的URL变量下面添加一些全局的简洁化变量,以此使得调用全局队列更加简便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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_queue和dispatch_get_main_queue。

1
2
3
4
5
6
dispatch_async(GlobalUserInitiatedQueue) {
   let overlayImage = self.faceOverlayImageFromImage(self.image)
   dispatch_async(GlobalMainQueue) {
     self.fadeInNewImage(overlayImage)
   }
 }

这样就使得派发队列的调用的代码更加具有可读性并很轻松地得知哪个队列正在被使用。

利用dispatch_after实现延迟

考虑一下你App的UX。你的App有没有使得用户在第一次打开App的时候不知道该干些什么而感到不知所措呢?: ]

假如在PhotoManager中没有任何一张照片的时候便向用户发出提醒应该是一个不错的主意。不管怎样,你还是要考虑一下用户在App主页面上的注意力:假如你的提醒显示得过快的话,用户没准在因为看着其他地方而错过它。

当用户第一次使用App的时候,在提醒显示前执行一秒钟的延迟应该足以吸引住用户的注意力。

在PhotoCollectionViewController.swift底部的showOrHideBarPrompt函数中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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!"
   }
  }
 }

当你的UICollectionView重载的时候,viewDidLoad函数中的showOrHideNavPrompt将被执行。解释如下:

  1. 你声明了一个代表具体延迟时间的变量。

  2. 你将等待delayInSeconds变量中设定的时间然后向主队列异步添加闭包。

编译并运行App。你会看到一个在很大程度上吸引用户注意力并告知他们该做些什么的细微延迟。

dispatch_after就像一个延迟的dispatch_async。你仍旧在实时运行的时候毫无操控权并且一旦dispatch_after返回后你也无法取消整个延迟任务。

还在思考如何适当的使用dispatch_after?

  • 自定义连续队列(Custom Serial Queue):当你在自定义连续队列上使用dispatch_after时一定要当心,此时最好不要放到主队列上执行。

  • 主队列(Main Queue[Serial]):这对于dispatch_after是个很好的选择;Xcode对此有一个不错的自动执行至完成的样板。

  • 并发队列(Concurrent Queue):在自定义并发队列上使用dispatch_after时同样要当心,即便你很少这么做。此时最好放到主队列上执行。

单例和线程安全

单例,不管你love it还是hate it,他们对于iOS都是非常重要的。: ]

一提到单例(Singleton)人们便觉得他们是线程不安全的。这么想的话也不是没有道理:单例的实例经常在同一时间内被多线程所访问。PhotoManager类便是一个单例,所以你要思考一下上面提到的问题。

两个需要考虑的情况,单例实例初始化时和实例读写时的线程安全性。

先考虑第一种情况。因为在swift是在全局范围内初始化变量,所以这种情况较为简单。在Swift中,当全局变量被首次访问调用时便被初始化,并且整个初始化过程具有原子操作性。由此,代码的初始化过程便成为一个临界区并且在其他线程访问调用全局变量前完成初始化。Swift到底是怎么做到的?其实在整个过程中,Swift通过dispatch_once函数使用了GCD。若想了解得更多的话请看这篇Swift官方Blog

在线程安全的模式下dispatch_once只会执行闭包一次。当一个在临界区执行的线程--向dispatch_once传入一个任务--在它结束运行前其它的线程都会被限制住。一旦执行完成,它和其他线程便不会再次在此区域执行。通过let把单例定义为全局定量的话,我们就可以保证这个变量的值在初始化后不会被修改。总之,Swift声明的所有全局定量都是通过线程安全的初始化得到的单例。

但我们还是要考虑读写问题。尽管Swift通过使用dispatch_once确保我们在线程安全的模式下初始化单例,但这并不能代表单例的数据类型同样具有线程安全性。举个例子,假如一个全局变量是一个类的实例,你仍可以在类内的临界区操控内部数据,这将需要利用其他的方式来保证线程安全性。

处理读取与写入问题

保证线程安全性的实例化不是我们处理单例时的唯一问题。假如一个单例属性代表着一个可变的对象,比如像PhotoManager 中的photos数组,那么你就需要考虑那个对象是否就有线程安全性。

在Swift中任何用let声明的变量都是一个只可读并线程安全的常量。但是用var声明的变量都是值可变且并线程不安全的。比如Swift中像Array和Dictionary这样的集合类型若被声明为值可变的话,它们就是线程不安全的。那Foundation中的NSArray线程是否安全呢?不一定!苹果还专门为那些线程非安全的Foundation类列了一个清单。

尽管多线程可以在不出现问题的情况下同时读取一个Array的可变实例,但当一个线程试图修改实例的时候另一个线程又试图读取实例,这样的话安全性可就不能被保证了。

在下面PhotoManager.swift中的addPhoto函数中找一找错误:

1
2
3
4
5
6
func addPhoto(photo: Photo) {
  _photos.append(photo)
  dispatch_async(dispatch_get_main_queue()) {
    self.postContentAddedNotification()
  }
}

这个写取方法修改了可变数组的对象。

再来看一看photos的property:

1
2
3
4
private var _photos: [Photo] = []
var photos: [Photo] {
  return _photos
}

当property的getter读取可变数组的时候它就是一个读取函数。调用者得到一份数组的copy并阻止原数组被不当修改,但这不能在一个线程调用addPhoto方法的同时阻止另一个线程回调photo的property的getter。

提醒:在上述代码中,调用者为什么不直接得到一份photos的copy呢?这是因为在Swift中,所有的参数和函数的返回值都是通过推测(Reference)或值传输的。通过推测进行传输和Objective-C中传输指针是一样的,这就代表着你可以访问调用原始对象,并且对于同一对象的推测后其任何改变都可以被显示出来。在对象的copy中通过值结果传值且对于copy的更改都不对原是对象造成影响。Swift默认以推测机制或结构体的值来传输类的实例。

Swift中的Array和Dictionary都是通过结构体来实现的,当你向前或向后传输这些实例的时候,你的代码将会执行很多次的copy。这时不要当心内存使用问题,因为这些Swift的集合类型(如Array、Dictionary)的执行过程都已被优化,只有在必要的时候才会进行copy。对于来一个通过值传输的Array实例来说,只有在被传输后才会进行其第一次修改。

这是一个常见的软件开发环境下的读写问题。GCD通过使用dispatch barriers提供了一个具有读/写锁的完美解决方案。

在使用并发队列时,dispatch barriers便是一组像连续性路障的函数。使用GCD的barrier API保证了被传输的闭包是在特定时间内、在特定队列上执行的唯一任务。这就意味着在派发的barrier前传输的任务必须在特定闭包开始执行前完成运行。

当闭包到达后,barrier便开始执行闭包并保证此段时间内队列不会再执行任何其他的闭包。特定闭包一旦完成执行,队列便会返回其默认的执行状态。GCD同样提供了具有同步与异步功能的barrier函数。

下面的图式描述了在多个异步任务中的barrier函数的运行效果:

5.jpg

dispatch barrier

需要注意的是在barrier执行前程序是以并发队列的形式运行,但当barrier一旦开始运行后,程序便以连续队列的形式运行。没错,barrier是这段特定时间内唯一被执行的任务。当barrier执行结束后,程序再次回到了普通的并发队列运行状态。

对于barrier函数我们做一些必要的说明:

  • 自定义连续队列(Custom Serial Queue):在这种情况下不是特别建议使用barrier,因为barrier在连续队列执行期间不会起到任何帮助。

  • 全局并发队列(Global Concurrent Queue):谨慎使用;当其他系统也在使用队列的时候,你应该不想把所有的队列都垄为自己所用。

  • 自定义并发队列(Custom Concurrent Queue):适用于涉及临界区及原子性的代码。在任何你想要保正设定(setting)或初始化具有线程安全性的情况下,barrier都是一个不错的选择。

从上面对于自定义并发序列解释可以得出结论,你得写一个自己的barrier函数并将读取函数和写入函数彼此分开。并发序列将允许多个读取过程同步运行。

打开PhotoManager.swift,在photos属性下给类文件添加如下的私有属性:

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

通过dispatch_queue_create函数初始化了一个名为concurrentPhotoQueue的并发队列。第一个参数是一个逆DNS风格的命名方式;其描述在debugging时会非常有用。第二个参数设定了你的队列是连续性的还是并发性的。

很多网上的实例代码中都喜欢给dispatch_queue_create的第二个参数设定为0或NULL。其实这是一种过时的声明连续分派队列的方法。你最好用你自己的参数设定它。

找到addPhoto函数并代替为以下代码:

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

你的新函数是这样工作的:

  1. 通过使用你自己的自定义队列添加写入过程,在不久后临界区执行的时候这将是你的队列中唯一执行的任务。

  2. 向数组中添加对象。只要这是一个barrier属性的闭包,那么它在concurrentPhotoQueue队列中绝不会和其他闭包同时运行。

  3. 最后你推送了一个照片添加完毕的消息。这个消息应该从主线程推送因为它将处理一些涉及UI的工作,所以你为这个消息以异步的形式向主线程派发了任务。

以上便处理好了写入方法的问题,但是你还要处理一下photos的读取方法。

为了保证写入方面的线程安全行,你需要在concurrentPhotoQueue队列中运行读取方法。因为你需要从函数获取返回值并且在读取任务返回前不会运行任何其他的任务,所以你不能向队列异步派发任务。

在这种情况下,dispatch_sync是一个不错的选择。

dispatch_sync可以同步传输任务并在其返回前等待其完成。使用dispatch_sync跟踪含有派发barrier的任务,或者在当你需要使用闭包中的数据时而要等待运行结束的时候使用dispatch_sync。

谨慎也是必要的。想象一下,当你对一个马上要运行的队列调用dispatch_sync时,这将造成死锁。因为调用要等到闭包B执行后才能开始运行,但是这个闭包B只有等到当前运行的且不可能结束的闭包A执行结束后才有可能结束。

这将迫使你时刻注意自己调用的的或是传入的队列。

来看一下dispatch_sync的使用说明:

  • 自定义连续队列(Custome Serial Queue):这种情况下一定要非常小心;假如一个队列中正在执行任务并且你将这个队列传入dispatch_sync中使用,这毫无疑问会造成死锁。

  • 主队列(Main Queue[Serial]):同样需要小心发生死锁。

  • 并发队列(Concurrent Queue):在对派发barrier执行同步工作或等待一个任务的执行结束后需要进行下一步处理的情况下,dispatch_sync是一个不错的选择。

依旧在PhotoManager.swift文件中,用以下代码替换原有的photos属性:

1
2
3
4
5
6
7
var photos: [Photo] {
  var photosCopy: [Photo]!
  dispatch_sync(concurrentPhotoQueue) { // 1
    photosCopy = self._photos // 2
  }
  return photosCopy
}

分布解释一下:

  1. 同步派发concurrentPhotoQueue使其执行读取功能。

  2. 储存照片数组至photosCopy并返回。

恭喜--你的PhotoManager单例现在线程安全了。不管现在是执行读取还是写入功能,你都可以保证整个单例在安全模式下运行。

队列可视化

还不能完全理解GCD的基础知识?接下来我们将在一个简单的示例中使用断点和NSLog功能确保你进一步理解GCD函数运行原理。

我将使用两个动态的GIF帮助你理解dispatch_async和dispatch_sync。在GIF的每步切换下,注意代码断点与图式的关系。

dispatch_sync重览

1
2
3
4
5
6
7
8
override func viewDidLoad() {
  super.viewDidLoad()
  dispatch_sync(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {
    NSLog("First Log")
  }
  NSLog("Second Log")
}

1433384310390198.png

dispatch_sync

分布解释:

  1. 主队列按顺序执行任务,下一个将要被执行的任务便是实例化包含viewDidLoad的UIViewController。

  2. 主队列开始执行viewDidLoad。

  3. dispatch_sync闭包添加至全局队列并在稍后被执行。在此闭包完成执行前主队列上的工作将被暂停。回调的闭包可以被并发执行并以FIFO的顺序添加至一个全局队列。这个全局队列还包含添加dispatch_sync闭包前的多个任务。

  4. 终于轮到dispatch_sync闭包执行了。

  5. 闭包执行结束后主队列开始恢复工作。

  6. viewDidLoad函数执行结束,主队列开始处理其他任务。

dispatch_sync函数向队列添加了一个任务并等待任务完成。 其实dispatch_async也差不多,只不过它不会等待任务完成便会返回线程。

dispatch_async重览

1
2
3
4
5
6
7
8
override func viewDidLoad() {
  super.viewDidLoad()
  dispatch_async(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {
    NSLog("First Log")
  }
  NSLog("Second Log")
}

7.gif

dispatch_async

  1. 主队列按顺序执行任务,下一个将要被执行的任务便是实例化包含viewDidLoad的`UIViewControl。

  2. 主队列开始执行viewDidLoad。

  3. dispatch_async闭包添加至全局队列并在稍后被执行。

  4. 向全局队列添加dispatch_async闭包后viewDidLoad函数继续运行,主线程继续其剩余的任务。与此同时全局队列是并发性的处理它的任务的。可被并发执行的闭包将以FIFO的顺序添加至全局队列。

  5. 通过dispatch_async添加的闭包开始执行。

  6. dispatch_async闭包执行结束,并且所有的NSLog语句都已被显示在控制台上。

在这个例子中,第二个NSLog语句执行后第一个NSLog语句才执行。这种情况并不是每次都会发生的--这取决于硬件在给定的时间内所处理的工作,并且你对于哪个语句会先被执行一无所知且毫无控制权。没准“第一个”NSLog就会作为第一个log出现。

Where to Go From Here?

在这篇教程中,你学会了如何让你的代码具有线程安全性和如何在CPU高密度处理多个任务的时候获取主线程的响应。

你可以从这里下载GooglyPuff的完整代码,在下一节教程中你将会继续在这个工程中进行修改。

假如你打算优化你的App,我觉得你真的该使用Instruments中的Time Profile. 具体教程请查看这篇How To Use Instruments

 

本文转载自:http://www.cocoachina.com/ios/20150609/12072.html

posted on 2016-06-22 16:24  iosblog's  阅读(196)  评论(0编辑  收藏  举报

导航