博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
GCD技术小结
阅读量:4935 次
发布时间:2019-06-11

本文共 20236 字,大约阅读时间需要 67 分钟。

什么是 GCD
GCD 是 libdispatch 的市场名称,而 libdispatch 作为 Apple 的一个库,为并发代码在多核硬件(跑 iOS 或 OS X )上执行提供有力支持。它具有以下优点:
1.GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
2.GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
3.GCD 具有在常见模式(例如单例)上用更高性能的原语去优化你的代码的潜在能力。
 
GCD 术语
Serial vs. Concurrent 串行 vs. 并发
这些术语描述当任务相对于其它任务被执行,任务串行执行就是每次只有一个任务被执行,任务并发执行就是在同一时间可以有多个任务被执行。
Synchronous vs. Asynchronous 同步 vs. 异步
在 GCD 中,这些术语描述当一个函数相对于另一个任务完成,此任务是该函数要求 GCD 执行的。一个同步函数只在完成了它预定的任务后才返回。
一个异步函数,刚好相反,会立即返回,预定的任务会完成但不会等它完成。因此,一个异步函数不会阻塞当前线程去执行下一个函数。
Critical Section 临界区
就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很可能会变质。
Race Condition 竞态条件
这种状况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。
Deadlock 死锁
两个(有时更多)东西——在大多数情况下是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。
Thread Safe 线程安全
线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。
Context Switch 上下文切换
一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。
Concurrency vs Parallelism 并发与并行
并发和并行通常被一起提到,所以值得花些时间解释它们之间的区别。
并发代码的不同部分可以“同步”执行。然而,该怎样发生或是否发生都取决于系统。多核设备通过并行来同时执行多个线程;然而,为了使单核设备也能实现这一点,它们必须先运行一个线程,执行一个上下文切换,然后运行另一个线程或进程。这通常发生地足够快以致给我们并发执行地错觉。虽然你可以编写代码在 GCD 下并发执行,但 GCD 会决定有多少并行的需求。并行要求并发,但并发并不能保证并行。
Queues 队列
GCD 提供有 dispatch queues 来处理代码块,这些队列管理你提供给 GCD 的任务并用 FIFO 顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。
所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行的访问它们。 GCD 的优点是显而易见的,即当你了解了调度队列如何为你自己代码的不同部分提供线程安全。关于这一点的关键是选择正确类型的调度队列和正确的调度函数来提交你的工作。
Serial Queues 串行队列
GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。
由于在串行队列中不会有两个任务并发运行,因此不会出现同时访问临界区的风险;相对于这些任务来说,这就从竞态条件下保护了临界区。所以如果访问临界区的唯一方式是通过提交到调度队列的任务,那么你就不需要担心临界区的安全问题了。
Concurrent Queues 并发队列
在并发队列中的任务能得到的保证是它们会按照被添加的顺序开始执行,但这就是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。这完全取决于 GCD 。
下图展示了一个示例任务执行计划,GCD 管理着四个并发任务: 
 
注意 Block 1,2 和 3 都立马开始运行,一个接一个。在 Block 0 开始后,Block 1等待了好一会儿才开始。同样, Block 3 在 Block 2 之后才开始,但它先于 Block 2 完成。
何时开始一个 Block 完全取决于 GCD 。如果一个 Block 的执行时间与另一个重叠,也是由 GCD 来决定是否将其运行在另一个不同的核心上,如果那个核心可用,否则就用上下文切换的方式来执行不同的 Block 。
有趣的是, GCD 提供给你至少五个特定的队列,可根据队列类型选择使用。
Queue Types 队列类型
首先,系统提供给你一个叫做 主队列(main queue) 的特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发生消息给 UIView 或发送通知的。
系统同时提供给你好几个并发队列。它们叫做 全局调度队列(Global Dispatch Queues) 。目前的四个全局队列有着不同的优先级:background、low、default 以及 high。要知道,Apple 的 API 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。
最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有五个队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。
 
实战
用 dispatch_async 处理后台任务
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 ,UI更新        });     });

 1. 首先将工作从主线程移到全局线程。因为这是一个 dispatch_async() ,Block 会被异步地提交,意味着调用线程地执行将会继续。

 2. 添加一个新的 Block 到主线程。记住——你必须总是在主线程访问 UIKit 的类。
 3. 最后,更新 UI。
正如之前提到的, dispatch_async 添加一个 Block 都队列就立即返回了。任务会在之后由 GCD 决定执行。当你需要在后台执行一个基于网络或 CPU 紧张的任务时就使用 dispatch_async ,这样就不会阻塞当前线程。
下面是一个关于在 dispatch_async 上如何以及何时使用不同的队列类型的快速指导:
1. 自定义串行队列:当你想串行执行后台任务并追踪它时就是一个好选择。这消除了资源争用,因为你知道一次只有一个任务在执行。注意若你需要来自某个方法的数据,你必须内联另一个 Block 来找回它或考虑使用 dispatch_sync。
2. 主队列(串行):这是在一个并发队列上完成任务后更新 UI 的共同选择。要这样做,你将在一个 Block 内部编写另一个 Block 。以及,如果你在主队列调用 dispatch_async 到主队列,你能确保这个新任务将在当前方法完成后的某个时间执行。
3. 并发队列:这是在后台执行非 UI 工作的共同选择。
 
使用 dispatch_after 延后工作
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     [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; });

 1. 你声明了一个变量指定要延迟的时长。

 2. 然后等待 delayInSeconds 给定的时长,再异步地添加一个 Block 到主线程。

dispatch_after 工作起来就像一个延迟版的 dispatch_async 。你依然不能控制实际的执行时间,且一旦 dispatch_after 返回也就不能再取消它。
不知道何时适合使用 dispatch_after ?
1. 自定义串行队列:在一个自定义串行队列上使用 dispatch_after 要小心。你最好坚持使用主队列。
2. 主队列(串行):是使用 dispatch_after 的好选择;Xcode 提供了一个不错的自动完成模版。
3. 并发队列:在并发队列上使用 dispatch_after 也要小心;这样做就比较罕见。还是在主队列做这些操作吧。
 
让你的单例线程安全
单例,不论喜欢还是讨厌,它们在 iOS 上的流行情况就像网上的猫。
一个常见的担忧是它们常常不是线程安全的。这个担忧十分合理,基于它们的用途:单例常常被多个控制器同时访问。
 单例的线程担忧范围从初始化开始,到信息的读和写。PhotoManager 类被实现为单例——它在目前的状态下就会被这些问题所困扰。要看看事情如何很快地失去控制,你将在单例实例上创建一个控制好的竞态条件。
@property(nonatomic,strong) NSMutableArray photosArray;//...+ (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]; });
这里创建了多个异步并发调用来实例化单例,然后引发上面描述的竞态条件。
编译并运行项目;查看控制台输出,你会看到多个单例被实例化,如下所示: 
注意到这里有好几行显示着不同地址的单例实例。这明显违背了单例的目的,对吧?
这个输出向你展示了临界区被执行多次,而它只应该执行一次。现在,固然是你自己强制这样的状况发生,但你可以想像一下这个状况会怎样在无意间发生。
要纠正这个状况,实例化代码应该只执行一次,并阻塞其它实例在 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 的一个实例而不会产生问题,但当一个线程正在读取时让另外一个线程修改数组就是不安全的。你的单例在目前的状况下不能预防这种情况的发生。
要分析这个问题,看看 PhotoManager.m 中的 addPhoto:,如下:
- (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 的障碍(barrier)API 确保提交的 Block 在那个特定时间上是指定队列上唯一被执行的条目。这就意味着所有的先于调度障碍提交到队列的条目必能在这个 Block 执行前完成。
当这个 Block 的时机到达,调度障碍执行这个 Block 并确保在那个时间里队列不会执行任何其它 Block 。一旦完成,队列就返回到它默认的实现状态。 GCD 提供了同步和异步两种障碍函数。
下面是你何时会——和不会——使用障碍函数的情况:
1. 自定义串行队列:一个很坏的选择;障碍不会有任何帮助,因为不管怎样,一个串行队列一次都只执行一个操作。
2. 全局并发队列:要小心;这可能不是最好的主意,因为其它系统可能在使用队列而且你不能垄断它们只为你自己的目的。
3. 自定义并发队列:这对于原子或临界区代码来说是极佳的选择。任何你在设置或实例化的需要线程安全的事物都是使用障碍的最佳候选。
由于上面唯一像样的选择是自定义并发队列,你将创建一个你自己的队列去处理你的障碍函数并分开读和写函数。且这个并发队列将允许多个多操作同时进行。
打开 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];              });         });     } }

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

1. 在执行下面所有的工作前检查是否有合法的相片。
2. 添加写操作到你的自定义队列。当临界区在稍后执行时,这将是你队列中唯一执行的条目。
3. 这是添加对象到数组的实际代码。由于它是一个障碍 Block ,这个 Block 永远不会同时和其它 Block 一起在 concurrentPhotoQueue 中执行。
4. 最后你发送一个通知说明完成了添加图片。这个通知将在主线程被发送因为它将会做一些 UI 工作,所以在此为了通知,你异步地调度另一个任务到主线程。
这就处理了写操作,但你还需要实现 photos 读方法并实例化 concurrentPhotoQueue 。
在写者打扰的情况下,要确保线程安全,你需要在 concurrentPhotoQueue 队列上执行读操作。既然你需要从函数返回,你就不能异步调度到队列,因为那样在读者函数返回之前不一定运行。
在这种情况下,dispatch_sync 就是一个绝好的候选。
dispatch_sync() 同步地提交工作并在返回前等待它完成。使用 dispatch_sync 跟踪你的调度障碍工作,或者当你需要等待操作完成后才能使用 Block 处理过的数据。如果你使用第二种情况做事,你将不时看到一个 __block 变量写在 dispatch_sync 范围之外,以便返回时在 dispatch_sync 使用处理过的对象。
但你需要很小心。想像如果你调用 dispatch_sync 并放在你已运行着的当前队列。这会导致死锁,因为调用会一直等待直到 Block 完成,但 Block 不能完成(它甚至不会开始!),直到当前已经存在的任务完成,而当前任务无法完成!这将迫使你自觉于你正从哪个队列调用——以及你正在传递进入哪个队列。
下面是一个快速总览,关于在何时以及何处使用 dispatch_sync :
1. 自定义串行队列:在这个状况下要非常小心!如果你正运行在一个队列并调用 dispatch_sync 放在同一个队列,那你就百分百地创建了一个死锁。
2. 主队列(串行):同上面的理由一样,必须非常小心!这个状况同样有潜在的导致死锁的情况。
3. 并发队列:这才是做同步工作的好选择,不论是通过调度障碍,或者需要等待一个任务完成才能执行进一步处理的情况。
继续在 PhotoManager.m 上工作,用下面的实现替换 photos :
- (NSArray *)photos {     __block NSArray *array; // 1     dispatch_sync(self.concurrentPhotoQueue, ^{ // 2         array = [NSArray arrayWithArray:_photosArray]; // 3     });     return array; }
1. __block 关键字允许对象在 Block 内可变。没有它,array 在 Block 内部就只是只读的,你的代码甚至不能通过编译。
2. 在 concurrentPhotoQueue 上同步调度来执行读操作。
3. 将相片数组存储在 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 队列的虚拟回顾 
下面提供了两个 GIF动画来帮助你巩固对 dispatch_async 和 dispatch_sync 的理解。包含在每个 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"); }
 
 
下面是图中几个步骤的说明:
1. 主队列一路按顺序执行任务——接着是一个实例化 UIViewController 的任务,其中包含了 viewDidLoad 。
2. viewDidLoad 在主线程执行。
3. 主线程目前在 viewDidLoad 内,正要到达 dispatch_sync 。
4. dispatch_sync Block 被添加到一个全局队列中,将在稍后执行。进程将在主线程挂起直到该 Block 完成。同时,全局队列并发处理任务;要记得 Block 在全局队列中将按照 FIFO 顺序出列,但可以并发执行。
5. 全局队列处理 dispatch_sync Block 加入之前已经出现在队列中的任务。
6. 终于,轮到 dispatch_sync Block 。
7. 这个 Block 完成,因此主线程上的任务可以恢复。
8. 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"); }
 
1.主队列一路按顺序执行任务——接着是一个实例化 UIViewController 的任务,其中包含了 viewDidLoad 。
2. viewDidLoad 在主线程执行。
3.主线程目前在 viewDidLoad 内,正要到达 dispatch_async 。
4.dispatch_async Block 被添加到一个全局队列中,将在稍后执行。
5.viewDidLoad 在添加 dispatch_async 到全局队列后继续进行,主线程把注意力转向剩下的任务。同时,全局队列并发地处理它未完成地任务。记住 Block 在全局队列中将按照 FIFO 顺序出列,但可以并发执行。
6.添加到 dispatch_async 的代码块开始执行。
7.dispatch_async Block 完成,两个 NSLog 语句将它们的输出放在控制台上。
在这个特定的实例中,第二个 NSLog 语句执行,跟着是第一个 NSLog 语句。并不总是这样——着取决于给定时刻硬件正在做的事情,而且你无法控制或知晓哪个语句会先执行。“第一个” NSLog 在某些调用情况下会第一个执行。
 

Dispatch Groups(调度组)

Dispatch Group 会在整个组的任务都完成时通知你。这些任务可以是同步的,也可以是异步的,即便在不同的队列也行。而且在整个组的任务都完成时,Dispatch Group 可以用同步的或者异步的方式通知你。因为要监控的任务在不同队列,那就用一个 dispatch_group_t 的实例来记下这些不同的任务。当组中所有的事件都完成时,GCD 的 API 提供了两种通知方式。

第一种是 dispatch_group_wait ,它会阻塞当前线程,直到组里面所有的任务都完成或者等到某个超时发生。

场景描述:PhotoManager.m中有一个下载照片的方法。一次需要下载3张图片,且图片是通过异步的方法进行下载。而我们在图片被下载完成后需要提示用户图片已经下载完成,故需要监听每个图片的下载完成的进度。如下:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1          __block NSError *error;         dispatch_group_t downloadGroup = dispatch_group_create(); // 2          for (NSInteger i = 0; i < 3; i++) {             NSURL *url;             switch (i) {                 case 0:                     url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];                     break;                 case 1:                     url = [NSURL URLWithString:kSuccessKidURLString];                     break;                 case 2:                     url = [NSURL URLWithString:kLotsOfFacesURLString];                     break;                 default:                     break;             }              dispatch_group_enter(downloadGroup); // 3             Photo *photo = [[Photo alloc] initwithURL:url                                   withCompletionBlock:^(UIImage *image, NSError *_error) {                                       if (_error) {                                           error = _error;                                       }                                       dispatch_group_leave(downloadGroup); // 4                                   }];              [[PhotoManager sharedManager] addPhoto:photo];         }         dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5         dispatch_async(dispatch_get_main_queue(), ^{ // 6             if (completionBlock) { // 7                 completionBlock(error);             }         });     }); }

按照注释的顺序,你会看到:

1. 因为你在使用的是同步的 dispatch_group_wait ,它会阻塞当前线程,所以你要用 dispatch_async 将整个方法放入后台队列以避免阻塞主线程。

2. 创建一个新的 Dispatch Group,它的作用就像一个用于未完成任务的计数器。

3. dispatch_group_enter 手动通知 Dispatch Group 任务已经开始。你必须保证 dispatch_group_enter 和 dispatch_group_leave 成对出现,否则你可能会遇到诡异的崩溃问题。

4. 手动通知 Group 它的工作已经完成。再次说明,你必须要确保进入 Group 的次数和离开 Group 的次数相等。

5. dispatch_group_wait 会一直等待,直到任务全部完成或者超时。如果在所有任务完成前超时了,该函数会返回一个非零值。你可以对此返回值做条件判断以确定是否超出等待周期;然而,你在这里用 DISPATCH_TIME_FOREVER 让它永远等待。它的意思,勿庸置疑就是,永-远-等-待!这样很好,因为图片的创建工作总是会完成的。

6. 此时此刻,你已经确保了,要么所有的图片任务都已完成,要么发生了超时。然后,你在主线程上运行 completionBlock 回调。这会将工作放到主线程上,并在稍后执行。

7. 最后,检查 completionBlock 是否为 nil,如果不是,那就运行它。

解决方案还不错,但是总体来说,如果可能,最好还是要避免阻塞线程。你的下一个任务是重写一些方法,以便当所有下载任务完成时能异步通知你。

在我们转向另外一种使用 Dispatch Group 的方式之前,先看一个简要的概述,关于何时以及怎样使用有着不同的队列类型的 Dispatch Group :

1. 自定义串行队列:它很适合当一组任务完成时发出通知。

2. 主队列(串行):它也很适合这样的情况。但如果你要同步地等待所有工作地完成,那你就不应该使用它,因为你不能阻塞主线程。然而,异步模型是一个很有吸引力的能用于在几个较长任务(例如网络调用)完成后更新 UI 的方式。

3. 并发队列:它也很适合 Dispatch Group 和完成时通知。

Dispatch Group,第二种方式

上面的一切都很好,但在另一个队列上异步调度然后使用 dispatch_group_wait 来阻塞实在显得有些笨拙。是的,还有另一种方式……

在 PhotoManager.m 中找到 downloadPhotosWithCompletionBlock: 方法,用下面的实现替换它:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {     // 1     __block NSError *error;     dispatch_group_t downloadGroup = dispatch_group_create();       for (NSInteger i = 0; i < 3; i++) {         NSURL *url;         switch (i) {             case 0:                 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];                 break;             case 1:                 url = [NSURL URLWithString:kSuccessKidURLString];                 break;             case 2:                 url = [NSURL URLWithString:kLotsOfFacesURLString];                 break;             default:                 break;         }          dispatch_group_enter(downloadGroup); // 2         Photo *photo = [[Photo alloc] initwithURL:url                               withCompletionBlock:^(UIImage *image, NSError *_error) {                                   if (_error) {                                       error = _error;                                   }                                   dispatch_group_leave(downloadGroup); // 3                               }];          [[PhotoManager sharedManager] addPhoto:photo];     }      dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4         if (completionBlock) {             completionBlock(error);         }     }); }

 下面解释新的异步方法如何工作:

1. 在新的实现里,因为你没有阻塞主线程,所以你并不需要将方法包裹在 async 调用中。

2. 同样的 enter 方法,没做任何修改。

3. 同样的 leave 方法,也没做任何修改。

4. dispatch_group_notify 以异步的方式工作。当 Dispatch Group 中没有任何任务时,它就会执行其代码,那么 completionBlock 便会运行。你还指定了运行 completionBlock 的队列,此处,主队列就是你所需要的。

对于这个特定的工作,上面的处理明显更清晰,而且也不会阻塞任何线程。

看看 PhotoManager 中的 downloadPhotosWithCompletionBlock 方法。你可能已经注意到这里的 for 循环,它迭代三次,下载三个不同的图片。你的任务是尝试让 for 循环并发运行,以提高其速度。dispatch_apply 刚好可用于这个任务。

dispatch_apply 表现得就像一个 for 循环,但它能并发地执行不同的迭代。这个函数是同步的,所以和普通的 for 循环一样,它只会在所有工作都完成后才会返回。

当在 Block 内计算任何给定数量的工作的最佳迭代数量时,必须要小心,因为过多的迭代和每个迭代只有少量的工作会导致大量开销以致它能抵消任何因并发带来的收益。而被称为跨越式(striding)的技术可以在此帮到你,即通过在每个迭代里多做几个不同的工作。

那何时才适合用 dispatch_apply 呢?

1. 自定义串行队列:串行队列会完全抵消 dispatch_apply 的功能;你还不如直接使用普通的 for 循环。

2. 主队列(串行):与上面一样,在串行队列上不适合使用 dispatch_apply 。还是用普通的 for 循环吧。

3. 并发队列:对于并发循环来说是很好选择,特别是当你需要追踪任务的进度时。

回到 downloadPhotosWithCompletionBlock: 并用下列实现替换它:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {     __block NSError *error;     dispatch_group_t downloadGroup = dispatch_group_create();      dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) {          NSURL *url;         switch (i) {             case 0:                 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];                 break;             case 1:                 url = [NSURL URLWithString:kSuccessKidURLString];                 break;             case 2:                 url = [NSURL URLWithString:kLotsOfFacesURLString];                 break;             default:                 break;         }          dispatch_group_enter(downloadGroup);         Photo *photo = [[Photo alloc] initwithURL:url                               withCompletionBlock:^(UIImage *image, NSError *_error) {                                   if (_error) {                                       error = _error;                                   }                                   dispatch_group_leave(downloadGroup);                               }];          [[PhotoManager sharedManager] addPhoto:photo];     });      dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{         if (completionBlock) {             completionBlock(error);         }     }); }

你的循环现在是并行运行的了;在上面的代码中,在调用 dispatch_apply 时,你用第一次参数指明了迭代的次数,用第二个参数指定了任务运行的队列,而第三个参数是一个 Block。

要知道虽然你有代码保证添加相片时线程安全,但图片的顺序却可能不同,这取决于线程完成的顺序。 

在真机上运行新代码会稍微更快的得到结果。但我们所做的这些提速工作真的值得吗?

实际上,在这个例子里并不值得。下面是原因:

1. 你创建并行运行线程而付出的开销,很可能比直接使用 for 循环要多。若你要以合适的步长迭代非常大的集合,那才应该考虑使用 dispatch_apply。

2. 你用于创建应用的时间是有限的——除非实在太糟糕否则不要浪费时间去提前优化代码。如果你要优化什么,那去优化那些明显值得你付出时间的部分。你可以通过在 Instruments 里分析你的应用,找出最长运行时间的方法.

3. 通常情况下,优化代码会让你的代码更加复杂,不利于你自己和其他开发者阅读。请确保添加的复杂性能换来足够多的好处。

记住,不要在优化上太疯狂。你只会让你自己和后来者更难以读懂你的代码。

<完>

本文整理自CocoaChina:http://www.cocoachina.com/industry/20140515/8433.html

 

 

 

 

 

 

 

 
 
 
 
 
 
 
 
 
 

转载于:https://www.cnblogs.com/hjs89/p/4240386.html

你可能感兴趣的文章
转:LoadRunner脚本录制常见问题整理
查看>>
研发管理 SCRUM 与 Agile
查看>>
webgl之五彩光源
查看>>
Gamma in Direct3D 9
查看>>
【iOS学习笔记】02-Foundation Kit
查看>>
获取一年时间的sql
查看>>
一周动态2016-08-01
查看>>
TFS命令方式强制签入别人签出的文件
查看>>
迭代器
查看>>
[转载]程序员必须知道的10大基础实用算法及其讲解
查看>>
MapReduce设置参数防止超时
查看>>
sql中关闭自增,并插入数据
查看>>
【python cookbook】【字符串与文本】8.编写多行模式的正则表达式
查看>>
Spring 事务管理-只记录xml部分
查看>>
GS 服务器端开启webservice 远程调试的方法
查看>>
如何用C#将bmp图的位图数据读到数组中?
查看>>
WPF中的两个绑定场景
查看>>
第一章第一节———java复习计划
查看>>
内网(域)渗透.基本命令
查看>>
后台添加控件时,必须每次重画控件,才能从前台获取控件数据。
查看>>