转 Grand Central Dispatch 基础教程:Part 1/2 -swift
本文转载,原文地址:http://www.cocoachina.com/ios/20150609/12072.html
原文 Grand
Central Dispatch Tutorail for Swift: Part 1/2
原文作者:Bjrn
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 并行
并发和并行总是被同一时候提及,所以有必要解释一下两者间的差别。
并发代码中各个单独部分能够被"同一时候"执行。无论如何,这都由系统决定以何种方式执行。
具有多核处理器的设备通过并行的方式在同一时间内实现多线程间的工作;可是单核处理器设备仅仅能在同一时间内执行在单一线程上,并利用上下文切换的方式切换至其它线程以达到跟并行同样的工作效果。例如以下图所看到的。单核处理器设备执行速度快到形成了一种并行的假象。
并发 VS 并行
虽然你会在GCD下写出使用多线程的代码,但这仍由GCD来决定是否会使用并发机制。
并行机制包括着并发机制,但并发机制却不一定能保证并行机制的执行。
队列
GCD通过队列分配的方式来处理待运行的任务。这些队列管理着你提供给GCD待处理的任务并以FIFO的顺序进行处理。
这就得以保证第一个加进队列的任务会被首个处理,第二个加进队列的任务则被其次处理,其后则以此类推。
连续队列
连续队列中的任务每次运行仅仅一个,一个任务仅仅有在其前面的任务运行完成后才可開始运行。
例如以下图所看到的。你不会知道前一个任务结束到下一个任务開始时的时间间隔。
连续队列
每个任务的运行时间都是由GCD控制的;唯一一件你能够确保的事便是GCD会在同一时间内依照任务加进队列的顺序运行一个任务。
由于在连续队列中不同意多个任务同一时候执行,这就降低了同一时候訪问临界区的风险;这样的机制在多任务的资源竞争的过程中保护了临界区。
假如分配任务至分发队列是訪问临界区的唯一方式。那这就保证了的临界区的安全。
并发队列
并发队列中的任务依然以FIFO顺序開始执行。
。。但你能知道的也就这么多了!任务间能够以不论什么顺序结束,你不会知道下一个任务開始的时间也不会知道一段时间内正在执行任务的数量。由于,这一切都是由GCD控制的。
例如以下图所看到的。在GCD控制下的四个并发任务:
并发队列
须要注意的是,在任务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中选择照片,也能够通过网络从事先设置好的地址下载照片。
将project下载至本地后用Xcode打开并编译执行。它看起来是这种:
GooglyPuff
在project中共同拥有四个类文件:
-
PhotoCollectionViewController:这是App执行后显示的首个界面。它将显示全部被选照片的缩略图。
-
PhotoDetailViewController:它将处理将咕噜眼加入至照片的工作并将处理完成的照片显示在UIScrollView中。
-
Photo:一个包括着照片基本属性的协议,当中有image(未处理照片)、thumbnail(裁减后的照片)及status(照片可否使用状态);两个用来实现协议的类,DownloadPhoto将从一个NSURL实例中实例化照片。而AssetPhoto则从一个ALAsset实例中实例化照片。
-
PhotoManager:这个类将管理全部Photo类型对象。
使用dispatch_async处理后台任务
回到刚才执行的App后,通过自己的Photo Library加入照片或是使用Le internet下载一些照片。
须要注意的是当你点击PhotoCollectionViewController中的一个UICollectionViewCell后,界面切换至一个新的PhotoDetailViewController所用的时间。对于那些处理速度较慢的设备来说,处理一张较大的照片会产生一个很明显的延迟。
这样的情况下非常easy使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 } } } |
在这里解释一下上面改动的代码:
-
你首先将照片处理工作从主线程(main thread)移至一个全局队列(global queue)。
由于这是一个异步派发(dispatch_async的调用,闭包以异步的形式进行传输意味着调用的线程将会被继续运行。这样一来便会使viewDidLoad更早的在主线程上结束运行并使得整个载入过程更加流畅。与此同一时候。脸部识别的过程已经開始并在一段时间后结束。
-
这时脸部识别的过程已经结束并生成了一张新照片。
当你想用这张新照片来刷新你的UIImageView时,你能够向主线程加入一个新的闭包。
须要注意的是--主线程仅仅能用来訪问UIKit。
-
最后。你便用这张有着咕噜眼的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将被运行。
解释例如以下:
-
你声明了一个代表详细延迟时间的变量。
-
你将等待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函数的执行效果:
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() } } } |
你的新函数是这样工作的:
-
通过使用你自己的自己定义队列加入写入过程,在不久后临界区运行的时候这将是你的队列中唯一运行的任务。
-
向数组中加入对象。仅仅要这是一个barrier属性的闭包。那么它在concurrentPhotoQueue队列中绝不会和其它闭包同一时候执行。
-
最后你推送了一个照片加入完成的消息。这个消息应该从主线程推送由于它将处理一些涉及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 } |
分布解释一下:
-
同步派发concurrentPhotoQueue使其运行读取功能。
-
储存照片数组至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" ) } |
dispatch_sync
分布解释:
-
主队列按顺序运行任务,下一个将要被运行的任务便是实例化包括viewDidLoad的UIViewController。
-
主队列開始运行viewDidLoad。
-
dispatch_sync闭包加入至全局队列并在稍后被运行。在此闭包完毕运行前主队列上的工作将被暂停。
回调的闭包能够被并发运行并以FIFO的顺序加入至一个全局队列。
这个全局队列还包括加入dispatch_sync闭包前的多个任务。
-
最终轮到dispatch_sync闭包运行了。
-
闭包运行结束后主队列開始恢复工作。
-
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" ) } |
dispatch_async
-
主队列按顺序运行任务,下一个将要被运行的任务便是实例化包括viewDidLoad的`UIViewControl。
-
主队列開始运行viewDidLoad。
-
dispatch_async闭包加入至全局队列并在稍后被运行。
-
向全局队列加入dispatch_async闭包后viewDidLoad函数继续执行。主线程继续其剩余的任务。
与此同一时候全局队列是并发性的处理它的任务的。可被并发执行的闭包将以FIFO的顺序加入至全局队列。
-
通过dispatch_async加入的闭包開始运行。
-
dispatch_async闭包运行结束,而且全部的NSLog语句都已被显示在控制台上。
在这个样例中,第二个NSLog语句运行后第一个NSLog语句才运行。这样的情况并非每次都会发生的--这取决于硬件在给定的时间内所处理的工作,而且你对于哪个语句会先被运行一无所知且毫无控制权。
没准“第一个”NSLog就会作为第一个log出现。
Where to Go From Here?
在这篇教程中,你学会了怎样让你的代码具有线程安全性和怎样在CPU高密度处理多个任务的时候获取主线程的响应。
你能够从这里下载GooglyPuff的完整代码,在下一节教程中你将会继续在这个project中进行改动。
假如你打算优化你的App,我认为你真的该使用Instruments中的Time Profile. 详细教程请查看这篇How To Use Instruments。