虽然之前在工作中用到过GCD而且解决了不少问题,但是感觉对它还是有一定的恐惧,因为感觉自己并不懂他。都是在需要他的时候根据具体的业务需求,去网上搜索一下使用的方法。并没具体的、系统的学习过。所以想要彻底的学会它。(其实还是怕在找工作的时候,问道某些自己没有注意的点,很减分的。)
为什么要吧学习记录下来(对!是记录。90%的内容是照搬的原文章。如果以后牵扯到抄袭问题,文章就删除了🤦♀️)?因为我发现光是去看跟着写代码,发现学不到精髓。所以想要记录下来增强记忆(除了代码,剩余的部分都是手打)。加深理解,并希望有小伙伴看到本记录内容的时候,指出理解不对的地方。因为我看不到自己的后背。
还有一个记录下来的原因,那就是之前的学习方式就是这样,看一遍不如读一遍,读一遍不如写一遍。这样可以加深印象。
什么是GCD?
GCD是 libdispatch 的市场名称,而 libdispatch 作为Apple的一个库,未并发代码在多硬核硬件 (iOS或者OS X)上执行提供有力的支持。它具有一下优点:
- GCD能通过推迟昂贵计算任务并在后台运行他们来改善你的应用的响应速度
- GCD提供一个易于使用并发模型而不仅仅是锁和线程,以帮助我们避开并发陷阱
- GCD具有在常见模式(例如单例模式)上用更高性能的自带API优化你代码的潜在能力
GCD术语
要理解CGD, 你首先要了解线程和并发相关的几个概念。这两则都可能模糊和微妙,再开始学习GCD之前我们先来回顾一下。
Serial vs Concurrent 串行 vc 并发
任务串行就是每次只有一个任务被执行,任务并发执行就是同一时间可以多个任务被执行。
虽然这些术语被广泛使用,本教程你可以将任务设定为一个Block。不明白Block是什么?请看这个链接 iOS 5 教程中的如何使用 Block 。实际上,你也可以在GCD上使用函数指针,但是在大多场景中,这实际很难使用。Block就更加容易些。
Synchronous vc Asynchronous 同步 vc 异步
同步函数 只在完成了他预定的任务之后才返回。
异步任务 刚好相反,会直接返回。预定的任务完成,但是不会等待他完成。所以不会堵塞当前线程去执行下一个函数。
Critical Section 临界区
就是一段代码不能被并发执行。也就是说,同一段代码不能被两个线程同时执行。因为代码去操作一个共享资源,例如一个变量能被并发进程访问,那么他的值就不再可信。
Race Condition 静态条件
这种情况是指基于特定序号和时机的事件的软件系统以不受控制的方式运行的行为,例如程序并发任务执行的确切顺序,静态条件将无法预测这种行为,而不能通过代码排查立即发现。
Deadlock 死锁
两个(有时更多)东西--在大多情况下,线程--所谓的死锁使他们都卡住了,并等待对方完成或执行其他操作。第一个不能完成是因为她在等待第二个,第二个不能完成他是在等待第一个(有没有循环引用那味了?)。
Thread safe 线程安全
线程安全的代码能在多线程或者并发任务中被安全调用,而不会导致问题(崩溃,数据损坏等)。线程不安全的代码在某个时刻只能在一个上下文中运行,一个线程安全代码的例子是NSDictionary。你可以在同一时间在多个线程中使用它而不会有问题。另外一方面NSMutableDictionary就不是线程安全的,应该保证一次只有一个线程访问他。
Context Switch 上下文切换
上下文切换是指你在单个进程里执行切换不同的线程时存储和恢复执行状态的过程。这个过程在编写多任务应用时很普遍,单会带来一些额外的开销。
Concurrent vc Parallelism 并发和并行
并发和并行经常被一起提到, 所以值得花时间解释他们之间的区别。
并发代码的不同部分可以“同步”执行。然而该怎样发生或是否发生都取决与系统。多核设备通过并行来执行多个线程;然而,为了使单核设备也能实现这点,他们必须先运行一个线程,并执行一个上下文切换,然后执行另外一个线程或进程。这通常发生的比较快,以至于给我们并发执行的错觉,如图所示:
虽然你可以编写代码在GCD并发下执行,但GCD会决定有多少并行的需求。并行要求并发,但是并发不能保证并行。
更深入的观点是并发实际上是关于_构造_,当你在脑海中用GCD编写代码,你组织你的代码来暴露能同时运行的多个代码片段,已经不能同时的那些。如果你想要深入此主题看看这个由Rob Pike做的精彩的讲座 。//纯英文并且没有字幕🤦♀️
Queues 队列
GCD有 dispatch queues 来处理代码块,这些队列管理你提供给GCD的任务并用FIFO的顺序执行这些任务。这将保证第一个被添加到队列的任务被第一个执行,第二个被添加到队列的任务被第二个执行,如此直到队列的终点。
所有的调度队列(dispatch queues)都是线程安全的,你能从多个线程并行的访问它们。当你了解了调度队列如何为你自己的代码的不同部分提供线程安全后,GCD的有点就是显而易见的。关于这一点的关键是选择正确类型的调度列表和正确的调度函数来提交你的代码。
在本节不会看到两种调度队列,都是GCD提供的。然后看一下, 工作是如何被调度函数添加到队列的。
Serial Queues 串行队列
串行队列里的任务一次只执行一个,每个任务都是在前一个任务执行完之后再开始执行。而且你不知道一个Block的结束和下一个任务开始之间的时间长度,入下图所示:
这些任务执行的时机由GCD来控制;唯一能保证的就是一次只执行一个任务,而且是按照我们添加的顺序进行执行。
因为串行队列不会有两个任务并发执行, 因此不会出现同时访问临界区的风险。这就是从静态条件下保护了临界区。所以如果访问临界区的唯一方式,是通过提交到调度队列的任务,那么你就不用担心临界区的安全问题。
Concurrent Queues 并发队列
在并发队列的任务能保证的就是会按照添加的顺序执行,但这就是全部的保证了。任务可以会以任意顺序完成,你不会直到何时开始下一个任务,或者任意时刻有多少Block在运行,这一切都取决于GCD。
下图展示了一个示例任务执行计划,GCD管理着四个并发任务:
注意 block1,2和3都立马开始运行,一个接着一个在block0之后,block1在0之后好一会才开始。同样,block3在block2之后开始,但是他优先于block2完成。
何时开始一个block完全取决于GCD。如果block的执行时间和另外一个重叠,也是由GCD来决定是否将他运行到另外一个核心上,如果那个核心可以。否者就用上下文切换的方式来执行不同的block。
有趣的是,GCD提供给你至少五个特定的队列,可以跟队列类型选择使用。
Queues Types 队列类型
首先,系统给你提供了一个叫做 主队列(main queue)的特殊队列。和其他串行队列一样,这个队列一次只执行一个。然而,他能保证所有的任务都在主队列执行,而且主队列是唯一可用来更新UI的线程。这个队列就是用于发送消息给UIView和发送通知的。
系统提供好几个并发队列,他们叫做 全局调度队列(Global Dispatch Queues)。目前的四个队列有着不同的优先级:background,low,default以及high。要知道,Apple的API也会使用这些队列,所以你添加任何任务,都不可能是这些队列的唯一的任务。
最后,你也可以创建自己的串行或并发队列。这就是说至少有五个队列任你处置:主队列,四个全局调度队列,和你自己创建的队列。
以上就是调度队列的大框架!
GCD的“艺术”归纳为选择合适的队列来调度函数以提交你的工作。体验这一点最好的方式就是,走一遍下面的例子,我们会在沿途提供一些常见性的建议。
入门
既然本教程的目的是优化且安全的使用GCD调用来自不同线程的代码。那么你将从一个近乎完成的GooglyPuff的项目入手。
GooglyPuff是一个没有优化,且线程不好全的应用。他使用Code Image的人脸检测API来覆盖一堆曲棍球眼镜到被检测到的人脸上。对于基本的图像,可以从相机胶卷选择,或用预备好的URL从网络下载。
下载完成之后,运行起来(项目预备好的图片链接是国外的无法访问,在百度上找个小美女/小帅哥的照片链接放上去就行了)
也可以使用自己相册的相片。
如图:
注意当你选择 Le Intermet 时,会有一个UIAlertView过早的弹出,你将会在本教程的第二个环节修复这个问题。
这个项目有四个有趣的类:
- PhotoCollectionViewController:它是应用开始的第一个控制器,它用于缩略图的方式展示所有选择的图片。
- PhotoDetailViewController:它执行把一个曲棍球眼镜添加到图片上的逻辑,并用一个ScrollView来展示结果图片。
- Photo:这是一个类簇,它根据一个URL的实例或一个ALAsset的实例来实例化照片,这个类提供图像,缩略图,以及从URL下载的状态。
- PhotoManager:他管理所有Photo的实例
用 dispatch_async 处理后台任务
回到应用并使用你的相机/相册添加一些照片,或者点击 Le Intermet 下载一些图片
再按下PhotoCollectionViewController的某一个 cell 到生成一个新的 PhotoDetailViewController 之间花了比较多的时间;你会注意到有一个明显的滞后,特别是在比较慢的设备的上查看比较大的图。
在重构 UIViewController 的 viewDidLoad时容易添加太多杂乱的工作(to much clutter),这通常会使控制器出现前有更长的等待时间。如果可能,最好是卸下一些工作到后台,如果他们不是必须要运行在加载时间里。
这听起来像是 dispatch_async 能做的事!
打开PhotoDetailViewController 并用下面的代码代替 viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
NSAssert(_image, @"Image not set; required to use view controller");
self.photoImageView.image = _image;
//Resize if neccessary to ensure it's not pixelated
if (_image.size.height <= self.photoImageView.bounds.size.height &&
_image.size.width <= self.photoImageView.bounds.size.width) {
[self.photoImageView setContentMode:UIViewContentModeCenter];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1
UIImage *overlayImage = [self faceOverlayImageFromImage:_image];
dispatch_async(dispatch_get_main_queue(), ^{ // 2
[self fadeInNewImage:overlayImage]; // 3
});
});
}
下面来说明新代码所做的事情:
- 首先将工作从主线程移到全局线程,因为这是一个
dispatch_async(), Block会被异步的提交,意味着调用现成的执行将会继续,这就使得viewDidLoad将会在主线程更早的完成,这过程感觉起来更加快速。同时一个人脸检测过程会启动,并会在稍后完成。 - 在这里,人脸检测完成,并生成一个新的图像。既然你需要使用新的图像更新UIImageView,那么你就要添加一个新的Block到主线程。切记——你必须总是在主线程中访问
UIKit的类。 - 最后使用
fadeInNewImage:更新UI, 他执行一个淡入过程切换到新的曲棍球眼镜图像。
编译并运行你的应用,选择一个图像,你会注意到视图控制器加载的速度明显变快,曲棍球眼镜稍后就会加上。这给应用带来了不错的效果,和之前的现实区别巨大。
进一步,你试着加载超大的图像,应用不会再加载视图上“挂住”,这就是使得应用有良好的伸缩性。
正如之前提到的,dispatch_async() 在添加一个Block后就立即返回了。任务会在之后由GCD决定执行。当你需要在后台执行一个基于网络或者CPU紧张的任务时就使用 dispatch_async。这样就不会阻塞主线程。
下面是一个关于在 dispatch_async 上如何以及何时使用不同的队列类型的快速指导:
- 自定义串行队列:当你想要串行执行后台任务并追踪他时就是一个好的选择。这消除了资源争用,因为你知道一次只有一个任务在执行。注意,如果你需要来自于另外一个方法的数据,你必须内联另外一个Block来回找或考虑
dispatch_sync . - 主队列(串行):这是在并发队列上完成任务后的共同选择。你这样做,你在一个Block内部编写另外一个Block。以及,如果你在主队列调用
dispatch_async到主队列(这句话没看懂),你能确保这个新任务将在当前方法完成后的某个时间完成。 - 并发队列:这是在后台执行非UI工作的共同选择。
使用 dispatch_after 延迟操作
稍微考虑一下应用的UX。用户第一次打开应用时,是否会困惑不知道该做什么?你是这样吗?
如果用户的 PhotoManager 里还没有照片,那么展示一个提示会使一个好主意。然后,你同样要考虑用户的眼睛会如何在屏幕上浏览,如果你太快的显示一个提示,他们的眼镜还在徘徊在视图的其他地方,他们很可能会错过它。
现实提示之前延迟一秒就足够用户捕捉到用户的注意。
添加如下代码到 PhotoCollectionViewController.m 中 showOrHideNavPrompt 的废止实现里面:
- (void)showOrHideNavPrompt
{
NSUInteger count = [[PhotoManager sharedManager] photos].count;
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2
if (!count) {
[self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"];
} else {
[self.navigationItem setPrompt:nil];
}
});
}
showOrHideNavPrompt 在 viewDidLoad 中执行,以及UICollectionView 被重新加载的任何时候。按照注释数字顺序看:
- 你指定了变量要延长的时长。
- 等待 delayInSeconds 的时长,再异步的添加一个Block到主线程里面。
编译并运行你的应用,应该有一个轻微的延迟,这有助于抓住用户的注意力并展示所要做的事情。
读者注释:这个例子我试自己在测试的时候,发现并不会展示弹窗,为什么?因为第一次运行的时候文字并没有添加到导航条上,具体原因不知!可以滑出手机通知栏,然后把通知栏关掉就可以出现了。
dispatch_after 工作起来像是一个延迟版的 dispatch_async 。你依然不能控制实际的执行时间,且一旦dispatch_after返回就再也不能取消他。
不知道何时使用 dispatch_after?
- 自定义串行队列:在一个自定义串行队列上用
dispatch_after要小心,你最好坚持使用主队列。(我不知道为什么要尽量在主队列使用!后续了解) - 主队列(串行):是使用
dispatch_after的好选择,Xcode提供了一个不错的自动完成模板。 - 并发队列:在并发队列上使用
dispatch_after也要小心,这样做本身就比较罕见,还是在主队列做这样的事情吧。
让你的单线程安全
单例,不管你喜欢还是讨厌,它他在iOS上流行程度就像是网上的猫。
一个常见的担忧是他们常常不是线程安全的。这个担忧十分合理,基于他们的用途:单例常常被几个控制器同时访问。
单例的线程担忧范围从创建开始,到信息的读和写。PhotoManager类被实现为单例——他在目前情况下就被这种问题困扰。要看看事情如何很快的失去控制,你将在单例实例上创建一个控制良好的静态条件。
导航到PhotoManager.m并找到sharedManager 它看起来如下:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
if (!sharedPhotoManager) {
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
}
return sharedPhotoManager;
}
当前状态下,代码相当简单。你创建了一个单例并初始化了一个叫做 的photosArray的NSMutableArray 属性。
然而,if 条件不是线程安全的,如果你多次调用这个方法,有一个可能性在某个线程(叫他线程A吧)进入到if语句之后在sharedPhotoManager分配内存之前发生了一个上下文切换,然后另一个线程(线程B)可能进入到了if, 分配单例的内存然后退出。
当系统切换上下文至线程A,又会分配一个单例实例的内存,然后退出。在那个时间点,你有了两个单例实例——着很明显不是你想要的。也就不能称为单例。
要让这个(竞态)条件必现,吧下面的方法替换掉PhotoManager.m中的sharedManager。
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
if (!sharedPhotoManager) {
[NSThread sleepForTimeInterval:2];
sharedPhotoManager = [[PhotoManager alloc] init];
NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
[NSThread sleepForTimeInterval:2];
sharedPhotoManager->_photosArray = [NSMutableArray array];
}
return sharedPhotoManager;
}
上面的代码中你使用 NSThread 的sleepForTimeInterval:类方法来强制发生一个上下文切换。
打开 AppDelegate.m 并添加如下代码到 application:didFinishLaunchingWithOptions:最开始处:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[PhotoManager sharedManager];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[PhotoManager sharedManager];
});
这里创建了多个异步并发调用来实例化单例,然后引发上面的竞态条件。
编译并运行项目,查看控制台输出,你会看到多个单例被实例化。如图所示:
读者注释:我在iOS13下并没有编译通过
但是这样就编译通过了:
不是很清除为什么,如果哪位大佬看到了并知道原因,请告诉我,感激不尽。好了下面进入正题。
注意到这里显示着好几行展示不同地址的单例实例。这明显违背了单例的目的,是吧?!
这个输出像你找事了临界区被执行了多次,而它应该执行一次。现在,虽然是你自己强制这样的状况发生。但你可以想象一下这个状况会怎样在无意间发生。
注意:基于你无法控制的系统事件,NSLog的数量有时会显示多个。线程问题及其难以调试,因为我们往往难以重视。
要纠正这个状况,实例代码应该只执行一次,并阻塞其他势力在if条件的临界区运行。就刚好就是dispatch_once能做的事。
在单例初始化方法中使用dispatch_once取代if条件判断,如下所示:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSThread sleepForTimeInterval:2];
sharedPhotoManager = [[PhotoManager alloc] init];
NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
[NSThread sleepForTimeInterval:2];
sharedPhotoManager->_photosArray = [NSMutableArray array];
});
return sharedPhotoManager;
}
编译并运行你的应用;查看控制台输出,你会看到有且仅有一个单例的实例——这就是你对单里的期望;
现在你已经明白了防止金泰条件的重要性,从AppDelegate.m中移除dispatch_async语句,并用下面的实现代替PhotoManager单例的初始化:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
});
return sharedPhotoManager;
}
dispatch_once()以线程安全的方式执行且执行其代码块一次。试图访问临界区(即传递给dispatch_once的代码)的不同的线程会在临界区已经有一个线程的情况下被堵塞,直到临界区完成为止。
需要记住的是,这只是让访问共享实例线程安全。他绝对没有让类本身线程安全。类中可能还有其他竞态条件,例如任何操纵内部数据的情况。这些需要用其他方式来保证线程安全,例如同步访问数据,你将在下面的几个小节看到。
处理读和写的问题
线程安全实例不是处理单例时的唯一问题。如果单例属性标识一个可变对象,那么你就需要考虑那个对象是否自身线程安全。
乳沟问题中的这个对象是一个Foundation容器类,那么答案是——“很可能是不安全”!Apple维护一个有用且有些心寒的列表,众多的Foundation类都不是线程安全的。NSMutableArray,已经用于你的单例,正在那个列表里休息。(NSMutableArray在线程不安全类的列表里)
虽然许多新城可以同时读取NSMutableArray的一个实例而不会产生问题,但当一个线程正在读取时让另外一个线程修改数组就是不安全的了。你的单例在目前状况下不能预防这种情况的发生。
要分析这个问题,看看PhotoManager.m中的addPhot:,转载如下:
- (void)addPhoto:(Photo *)photo
{
if (photo) {
[_photosArray addObject:photo];
dispatch_async(dispatch_get_main_queue(), ^{
[self postContentAddedNotification];
});
}
}
这是一个写方法,他修改一个私有可变数组对象。
现在看看photos, 转载如下:
- (NSArray *)photos
{
return [NSArray arrayWithArray:_photosArray];
}
这是所谓的读方法, 他读取可变数组。它为调用者生成一个不可变拷贝,防止调用者不当地改变数组,但这不能提供任何保护来对抗一个线程调用读方法photos的是同对另一个线程调用写方法addPhoto:。
这就是软件开发中经典的读写问题。GCD通过用dispatch barriers 创建一个读写锁提供了一个优雅的解决方案。
Dispatch barriers是一组函数,在并发队列上工作时扮演一个串行式的瓶颈。使用GCD的障碍(barriers)API确保提交的Block在那个特定时间上,是指定队列上,唯一被执行的条目。这就意味着所有的先于调度障碍提交到队列的条目必能在这个Block执行前完成。
当这个Block的时机到达,调度障碍执行这个Block,并保证在那段时间内队列不会执行任何别的Block。一旦完成。队列就会返回他默认的实现状态。GCD提供了同步和异步两种障碍函数。
下图现实了障碍函数对多个异步队列的影响:
注意正切部分的操作就如同一个正常并发队列。但,当障碍执行时,它的本质上就如同一个串行的队列。也就是障碍是唯一在执行的事物。在障碍完成后,队列回到一个正常并发队列的样子。
下面是你何时会——何时不会——使用障碍函数的情况:
- 自定义串行队列:一个很坏的选择;障碍不会有任何帮助,因为不管怎样,一个串行队列一次都只执行一个操作。
- 全局并发队列:要小心;这可能不是最好的主意,因为其他系统可能在使用队列,而且你不能为你自己的目的而垄断他们。
- 自定义并发队列:这对于原子或临界区代码来说是极佳的选择。任何你在设置或实例化的需要线程安全的事物都是使用障碍的最佳候选。
由于上面唯一像样的选择是自定义并发队列,你将创建一个你自己的队列去处理你的障碍函数,并分开读和写函数。且这个并发队列将允许多个多操作同时进行,
打开PhotoManager.m,添加如下私有属性到类扩展中:
@interface PhotoManager ()
@property (nonatomic,strong,readonly) NSMutableArray *photosArray;
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this
@end
找到addPhoto:并用下面的实现替换他
- (void)addPhoto:(Photo *)photo
{
if (photo) { // 1
dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2
[_photosArray addObject:photo]; // 3
dispatch_async(dispatch_get_main_queue(), ^{ // 4
[self postContentAddedNotification];
});
});
}
}
你信写的函数式这样工作的:
- 在执行下面所有的工作前,检查是否有合法的相片。
- 添加写操作到你的自定义队列。当临界点在稍后执行时,这将是你队列中唯一执行的条目。
- 这是添加对象到数组的实际代码。由于它是一个障碍Block,这个Block永远不会同时和其他Block一起在
concurrentPhotoQueue中执行。 - 最后一个发送一个通知说明完成了添加图片。这个通知将在主线程被发送,因为它将做一些UI工作,所以在此为了通知,你异步的调度一个任务到主线程。
这就处理了写的操作,但你还需要实现photos读方法并实例化concurrentPhotoQueue。
在写入的打扰下,要确保线程安全,你需要在concurrentPhotoQueue队列上执行读操作。既然你需要从函数返回,你就不能异步调度到队列,因为那样在读函数返回之前不一定运行。
在这种情况下,dispatch_sync就是一个绝好的候选。
dispatch_sync()同步地提交工作并在返回前等待他完成。
使用**dispatch_sync**跟踪你的调度障碍工作,或者当你需要等待操作完成后才能使用Block处理过的数据的时候。(黑体字这句话我读了好几遍都没有读通,个人理解:你可以使用dispatch_sync跟踪你的调度障碍工作,或者当你需要等待操作完成后,使用Block处理过的数据的时候使用它(它是指dispatch_sync)如果理解有误请大佬指出来,感谢。)如果你使用第二种情况做事,你将经常看到一个__block变量卸载dispatch_sync范围之外,以便返回时在dispatch_sync使用处理后的对象。
但你需要很小心。想象如果你将dispatch_sync放在你已经运行着的当前队列。这会导致死锁,因为调用dispatch_sync会一直等待,直到Block完成,但Block不能完成(他甚至都不会开始!),直到当前已存在的任务完成,而当前任务无法完成!这将迫使你,自觉于你正从哪个队列调用——以及你正在传递进入哪个队列。
下面是一个快速总览,更艳遇在何时以及何处使用dispatch_sync:
- 自定义串行队列:在这个状况下要非常消息!如果你正在运行一个队列,并调用
dispatch_sync放在同一个队列,那你就百分百创建了一个死锁。 - 主队列(串行):同上面的理由一样,必须非常小心!这个状况同样有潜在的导致死锁的情况。
- 并发队列:这才是做同步工作的好选择,不论是通过调度障碍,或者需要等待一个任务完成才能执行进一步的处理的情况。
继续在PhotoManager.m在工作,用下面的实现替换photo:
- (NSArray *)photos
{
__block NSArray *array; // 1
dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
array = [NSArray arrayWithArray:_photosArray]; // 3
});
return array;
}
这就是你的读函数。按顺序看看编过号的注释,有这些:
__block关键字允许对象在Block内可变。没有它,array在Block内部就只是只读的,你的代码甚至不能编译通过。- 在
concurrentPhotoQueue上同步调度来执行读操作。 - 将相片数组存储在
array内并返回它。
最后,你需要实例化你的concurrentPhotoQueue属性。修改sharedManager,以便像下面这样初始化队列
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
// ADD THIS:
sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue",
DISPATCH_QUEUE_CONCURRENT);
});
return sharedPhotoManager;
}
这里使用dispatch_queue_create初始化concurrentPhotoQueue为并发队列,对一个参数是反向DNS样式命名惯例;确保他是描述性的,将有助于调试。第二个参数制定你的队列是串行还是并行。
注意:当你在网上搜索例子时,你会经常看人们传递
0或者NULL给dispatch_queue_create的第二个参数。这是一个创建串行队列的过时方式;明确你的参数总是更好。
恭喜——你的PhotoManager单例已经是线程安全的了。不论你在何处以什么形式读写你的照片,你都有这样的自信,他会以安全的方式完成,不会受到任何惊吓。
A Visual Review of Queueing队列的视觉回顾
依然没有100%的掌握GCD要领???确保你可以使用GCD轻松地创建简单的例子,使用断点和NSLog语句保证自己明白当前发生的情况。
作者在下面提供了两个GIF动画来帮你巩固dispatch_sync和dispatch_async的理解。包含在GIF中的代码可以提供视觉辅助,仔细注意GIF左边代码断点走的每一步,以及右边相关队列的的状态。
dispatch_sync回顾
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"First Log");
});
NSLog(@"Second Log");
}
下面是几个步骤的说明:
-
主队列一路按顺序执行任务——接着就是实例化
UIViewCntroller,其中包含了viewDidLoad。 -
viewDidLoad在主线程执行 -
主线程目前在
viewDidLoad内,并即将要执行dispatch_sync。 -
dispatch_syncBlock被添加到一个全局队列,将在稍后执行。进程在主线程挂起直到该Block完成。同时,全局队列并发执行任务;要记得Block在全局队列是将按照FIFO顺序出列,但可以并发执行。 -
全局队列处理
dispatch_syncBlock加入之前就已经出现在队列的任务。 -
终于轮到了
dispatch_syncBlock。 -
这个Block执行完之后,因此主线程上的任务可以恢复。
-
viewDidLoad执行完毕,主队列将继续执行别的任务
dispatch_sync 添加任务到一个队列并等待,直到任务完成。dispatch_async也是将任务添加到一个队列,但是不同的是,他不会等待任务的完成,而是继续调用线程的其他任务。
dispatch_async回顾
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"First Log");
});
NSLog(@"Second Log");
}
下面是几个步骤的说明
-
主队列一路按顺序执行任务——接着就是实例化
UIViewCntroller,其中包含了viewDidLoad。 -
viewDidLoad在主线程执行 -
主线程目前在
viewDidLoad内,并即将要执行dispatch_async。 -
dispatch_asyncBlock被添加到一个全局队列,将在稍后执行。 -
主线程继续执行剩余任务,而不会等待
Block完成,同时全局队列并发地处理它未完成的任务,记住Block在全局队列中按照FIFO顺序出列,但是可以并发执行。 -
添加到
dispatch_async的代码块开始执行。 -
dispatch_async Block完成,两个NSLog,将他们的输出放在了控制台
在这个特例中,第二个NSLog语句会早于第一个NSLog语句。并不是总是这样——这取决于给定时刻硬件正在做的事情,而且你无法控制或知晓哪个语句先执行。第一个NSLog在某些调用情况下某第一个先执行。
下一步怎么走?
在本教程中,你学习了如何让你的代码线程安全,以及在执行 CPU 密集型任务时如何保持主线程的响应性。
你可以下载 GooglyPuff 项目,它包含了目前所有本教程中编写的实现。在本教程的第二部分,你将继续改进这个项目。
如果你计划优化你自己的应用,那你应该用 Instruments 中的 Time Profile 模版分析你的工作。对这个工具的使用超出了本教程的范围,你可以看看 如何使用Instruments 来得到一个很好的概述。
同时请确保在真实设备上分析,而在模拟器上测试会对程序速度产生非常不准确的印象。
在教程的下一部分,你将更加深入到 GCD 的 API 中,做一些更 Cool 的东西。
经历了几天才把这部分打完
一方面是因为公司这段时间比较忙(996),另外一方面是自己空闲时间大多都用在刷B站和打游戏了。
说一下感受,对我个人而言,手敲一遍比我读的两边都有效果,印象也更深了。
原作者的文章分为上下两段,下段我正在开始。