本文转载,原文地址: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中选择照片,也可以通过网络从事先设置好的地址下载照片。

GooglyPuff Swift Start 1

将工程下载至本地后用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
overridefuncviewDidLoad(){
super.viewDidLoad()
assert(image!=nil,"Imagenotset;requiredtouseviewcontroller")
photoImageView.image=image
//Resizeifneccessarytoensureit'snotpixelated
ifimage.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
letoverlayImage=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变量下面添加一些全局的简洁化变量,以此使得调用全局队列更加简便。

15
varGlobalMainQueue:dispatch_queue_t{
returndispatch_get_main_queue()
}
varGlobalUserInteractiveQueue:dispatch_queue_t{
returndispatch_get_global_queue(Int(QOS_CLASS_USER_INteraCTIVE.value),0)
}
varGlobalUserInitiatedQueue:dispatch_queue_t{
returndispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value),0)
}
varGlobalUtilityQueue:dispatch_queue_t{
returndispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value),0)
}
varGlobalBackgroundQueue:dispatch_queue_t{
returndispatch_get_global_queue(Int(QOS_CLASS_BACKGROUND.value),0)
}

回到PhotoDetailViewController中viewDidLoad函数中,用简洁变量代替dispatch_get_global_queue和dispatch_get_main_queue。

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

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

利用dispatch_after实现延迟

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

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

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

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

13
funcshowOrHideNavPrompt(){
letdelayInSeconds=1.0
letpopTime=dispatch_time(disPATCH_TIME_Now,
Int64(delayInSeconds*Double(NSEC_PER_SEC)))//1
dispatch_after(popTime,GlobalMainQueue){//2
letcount=PhotoManager.sharedManager.photos.count
ifcount>0{
self.navigationItem.prompt=nil
}else{
self.navigationItem.prompt="AddphotoswithfacestoGooglyifythem!"
}
}
}

当你的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函数中找一找错误:

6
funcaddPhoto(photo:Photo){
_photos.append(photo)
dispatch_async(dispatch_get_main_queue()){
self.postContentAddednotification()
}
}

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

再来看一看photos的property:

4
privatevar_photos:[Photo]=[]
varphotos:[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属性下给类文件添加如下的私有属性:

2
privateletconcurrentPhotoQueue=dispatch_queue_create(
"com.raywenderlich.GooglyPuff.photoQueue",disPATCH_QUEUE_CONCURRENT)

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

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

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

8
funcaddPhoto(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属性:

7
varphotos:[Photo]{
varphotoscopy:[Photo]!
dispatch_sync(concurrentPhotoQueue){//1
photoscopy=self._photos//2
}
returnphotoscopy
}

分布解释一下:

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

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

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

队列可视化

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

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

dispatch_sync重览

8
overridefuncviewDidLoad(){
super.viewDidLoad()
dispatch_sync(dispatch_get_global_queue(
Int(QOS_CLASS_USER_INteraCTIVE.value),0)){
NSLog("FirstLog")
}
NSLog("SecondLog")
}

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重览

super.viewDidLoad()
dispatch_async(dispatch_get_global_queue(
Int(QOS_CLASS_USER_INteraCTIVE.value),250)"> 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。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


效率成吨提升之代码生成器-蓝湖工具神器iOS,Android,Swift,Flutter
软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
【Audio音频开发】音频基础知识及PCM技术详解
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
见过仙女蹦迪吗?一起用python做个小仙女代码蹦迪视频
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
自定义ava数据集及训练与测试 完整版 时空动作/行为 视频数据集制作 yolov5, deep sort, VIA MMAction, SlowFast
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
【视频+源码】登录鉴权的三种方式:token、jwt、session实战分享
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App附源码和演示视频 超详细必看
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
采用MATLAB对正弦信号,语音信号进行生成、采样和恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
Keras深度学习实战40——音频生成
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
  • • 效率成吨提升之代码生成器-蓝湖工具神器…
  • • 【Audio音频开发】音频基础知识及PCM技…
  • • 见过仙女蹦迪吗?一起用python做个小仙…
  • • 【Android App】实战项目之仿抖音的短视…
  • • 自定义ava数据集及训练与测试 完整版 时…
  • • 【视频+源码】登录鉴权的三种方式:tok…
  • • 【Android App】实战项目之仿微信的私信…
  • • 零基础用Android Studio实现简单的本地…
  • • 采用MATLAB对正弦信号,语音信号进行生…
  • • Keras深度学习实战40——音频生成
  • • 视频实时行为检测——基于yolov5+deeps…
  • • 数电实验 数字电子钟设计 基于quartus …
  • • 腾讯会议使用OBS虚拟摄像头
  • • 文本生成视频Make-A-Video,根据一句话…
  • • 信号处理——MATLAB音频信号加噪、滤波
  • • 【新知实验室 - TRTC 实践】音视频互动…
  • • Keras深度学习实战39——音乐音频分类
  • • C++游戏game | 井字棋游戏坤坤版配资源…

转 Grand Central Dispatch 基础教程:Part 1/2 -swift的更多相关文章

  1. ios – 仅在异步函数完成执行后运行代码

    所以,例如:如果问题是你不知道要调用什么函数,你可以配置你周围的函数/对象,这样有人可以给你一个函数,然后你在我上面说“调用函数”的地方调用你的函数.例如:

  2. ios – 如何使用Objective C类中的多个参数调用Swift函数?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  3. ios – Swift中的UIView动画不起作用,错误的参数错误

    我正在尝试制作动画并使用下面的代码.我得到“无法使用类型’的参数列表调用’animateWithDuration'(FloatLiteralConvertible,延迟:FloatLiteralConvertible,选项:UIViewAnimationoptions,动画:()–>()–>$T4,完成:(Bool)–>(Bool)–>$T5)’“错误.这意味着我使用了错误的参数.我错了.请

  4. iOS:核心图像和多线程应用程序

    我试图以最有效的方式运行一些核心图像过滤器.试图避免内存警告和崩溃,这是我在渲染大图像时得到的.我正在看Apple的核心图像编程指南.关于多线程,它说:“每个线程必须创建自己的CIFilter对象.否则,你的应用程序可能会出现意外行为.”这是什么意思?我实际上是试图在后台线程上运行我的过滤器,所以我可以在主线程上运行HUD(见下文).这在coreImage的上下文中是否有意义?

  5. ios – 无法识别的选择器发送到实例NSTimer Swift

    解决方法让updateTime成为一个类方法.如果它是在一个纯粹的Swift类中,你需要在@objc前面说明该方法的声明,如:

  6. ios – 多个NSPersistentStoreCoordinator实例可以连接到同一个底层SQLite持久性存储吗?

    我读过的关于在多个线程上使用CoreData的所有内容都讨论了使用共享单个NSPersistentStoreCoordinator的多个NSManagedobjectContext实例.这是理解的,我已经使它在一个应用程序中工作,该应用程序在主线程上使用CoreData来支持UI,并且具有可能需要一段时间才能运行的后台获取操作.问题是NSPersistentStoreCoordinator会对基础

  7. ios – XCode断点应该只挂起当前线程

    我需要调试多线程错误.因此,为了获得生成崩溃的条件,我需要在代码中的特定点停止一个线程,并等待另一个线程到达第二个断点.我现在遇到的问题是,如果一个线程遇到断点,则所有其他线程都被挂起.有没有办法只停止一个线程,让其他线程运行,直到它们到达第二个断点?)其他更有趣的选择:当你点击第一个断点时,你可以进入控制台并写入这应该在该断点处暂停当前上下文中的线程一小时.然后在Xcode中恢复执行.

  8. ios – 在后台线程中写入Realm后,主线程看不到更新的数据

    >清除数据库.>进行API调用以获取新数据.>将从API检索到的数据写入后台线程中的数据库中.>从主线程上的数据库中读取数据并渲染UI.在步骤4中,数据应该是最新数据,但我们没有看到任何数据.解决方法具有runloops的线程上的Realm实例,例如主线程,updatetothelatestversionofthedataintheRealmfile,因为通知被发布到其线程的runloop.在后台

  9. iOS 7,用于断开调用的私有API CTCallDisconnect不起作用

    谢谢!

  10. ios – 监控CBPeripheral状态变化

    我在CoreBluetooth库中找不到任何暴露的东西,我想在CBperipheralstate发生变化时调用一个函数.现在我只有一个switch语句来检查外设状态,但它总是只返回连接或断开连接.我如何进入连接/断开连接的情况?

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部