原文 Grand Central Dispatch Tutorail for Swift: Part 1/2 html
原文做者:Bjrn Olav Ruud ios
译者:Ethan Joe swift
尽管Grand Central Dispatch(如下简称为GCD)已推出一段时间了,但并非全部人都明白其原理;固然这是能够理解的,毕竟程序的并发机制很繁琐,并且基于C的GCD的API对于Swift的新世界并非特别友好。 数组
在接下来的两节教程中,你将学习GCD的输入 (in)与输出 (out)。第一节将解释什么是GCD并了解几个GCD的基础函数。在第二节,你将学习几个更加进阶的GCD函数。 xcode
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才开始执行,但任务一、二、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中选择照片,也能够经过网络从事先设置好的地址下载照片。
将工程下载至本地后用Xcode打开并编译运行。它看起来是这样的:
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
}
}
}
|
在这里解释一下上面修改的代码:
你首先将照片处理工做从主线程(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的完整代码,在下一节教程中你将会继续在这个工程中进行修改。
假如你打算优化你的App,我以为你真的该使用Instruments中的Time Profile. 具体教程请查看这篇How To Use Instruments。
在下一节教程中,你将会利用更深层次的GCD的API去作些更Cool的东西。