iOS 详细介绍 GCD

什么是 GCD

GCD 存在于一个 名为 libdispatch 的类库中,这个苹果官方的类库提供了在 iOS 和 OS X 多核设备执行并发代码的支持。html

GCD 的优点

GCD 能够经过延迟可能须要花费大量时间的任务,并让他们在后台(background)运行,从而提升应用的响应速度。git

相对于线程和锁来讲,GCD 提供了一个更加易用的模板,从而避免发生并发问题(concurrency bug)。
对于相似单例(singletons)模式,GCD能够用来优化咱们的代码。github

GCD 相关术语

1. 串行 和 并行 (Serial VS Concurrent)

串行和并行描述的是任务之间如何运行。
串行任务每一次仅执行一个
并行任务能够多个同时执行数组

2. 同步 和 异步 (Synchronous VS Asynchronous)

同步方法仅会在一个任务完成后返回。
异步方法会当即返回,它会让这个任务执行完成,但不会等待任务完成。所以,异步方法不会阻塞当前线程。安全

3. Critical Section

指的是不能并发执行的一段代码(不能被两个线程同时访问)。这么作一般是由于一个共享数据(shared resource),例如一个变量,被并发进程访问后,进程之间可能都会对这个变量产生影响,可能会产生数据污染。网络

4. Race Condition(竞争情形)

当两个(多个)线程同时访问共享的数据时,会发生争用情形。第一个线程读取了一个变量。第二个线程也读取了这个变量的值。而后这两个线程同时操做了该变量,此时他们会发生竞争来看哪一个线程会最后写入这个变量。最后被写入的值将会被保留下来。多线程

5. 死锁

两个(多个)线程都要等待对方完成某个操做才能进行下一步,这时会发生死锁。例如,两个线程各自锁定了一个变量,而后他们想要锁定对方锁定的那个变量,这就产生了死锁。并发

6. 线程安全

一段线程安全的代码(一个线程安全的对象),能够同时被多个线程或并发的任务调用,不会产生问题。非线程安全的只能按次序被访问(调用)。举例来说,NSDictionary 是线程安全的,NSMutableDictionary 是非线程安全的。app

注意:全部的 Mutable 对象都是非线程安全的,全部的 Immutable 对象都是线程安全的。根本缘由是,Mutable 对象是能够被修改的,Immutable 对象时不能被修改的。使用 Mutable 对象要注意,必定要用同步锁来同步访问(@synchronized)。异步

互斥锁的优势:可以防止多线程抢夺形成的数据安全的问题。
缺点:实现互斥锁会消耗大量的资源。

同时,还有原子属性(atomic)也能够实现加锁。

  • atomic:原子属性,为 setter 方法加锁

  • nonatomic:非原子属性,不会为 setter 加锁

注意:全部属性都声明为 nonatomic, 客户端应尽可能避免多线程争夺同一资源。

7. context switch

当一个进程(process)中有多个线程(threads)来回切换时,context switch 用来记录执行状态。这样的进程和通常的多线程进程没有太大差异,但会产生一些额外的开销。

8. 并发 VS 并行(Parallelism)

并行是基于多核设备的,并行 必定是并发,但并发不必定是并行。

9. 队列(Queues)

GCD 提供了 dispatch queue 用来处理代码块,这些队列会管理你提供给GCD 的 tasks 而后按先进先出的顺序执行这些 tasks。

全部的 dispatch queue 都是线程安全的,咱们可使用多个线程同时访问一个 queue。当理解了 dispatch queue 如何为咱们的代码提供线程安全,GCD 的优势就很容易理解了。最关键的是要选择正确的 dispatch queue 和 dispatching function 来注册咱们的代码块。

10. 串行队列(serial queue)

串行队列中的任务(Tasks in serial queue),每次仅执行一个。执行顺序是,每一个任务仅会当上一个正在执行的任务结束后才开始执行。咱们没法知道一个任务结束时到下一个任务开始的时间。

这些任务的执行时间是由 GCD 控制的,GCD 可以保证的是,每次仅执行一个任务,这些任务的执行顺序是他们被添加进队列 (queue)的顺序。

11. 并发队列(concurrent queue)

并发队列中的任务会按照咱们添加任务的顺序来执行,但不保证完成了一个以后才开始下一个任务。这些任务可能以任何顺序结束,咱们也没法知道距下个任务(block)开始须要的时间,也不知道在某个时间段有多少个任务在执行。这些所有交由 GCD 控制。

何时开始执行一个任务(block)彻底由 GCD 控制。若是两个任务的执行时间有重叠,将有 GCD 来决定是否要将一种一个任务交给另外一个空闲状态的核心处理,仍是使用一个 context switch 来在两个 block 之间切换执行。

GCD 至少提供了5 种不一样的 queues 。

12. 队列类型(queue types)

(1) 首先,系统为咱们提供了一个特别的串行队列—— main queue。和其余串行队列同样,main queue 中的任务也是每次仅执行一个。main queue 保证了队列中全部的任务都会在主线程中进行。这个队列就是用来给 UIView 发送消息,或者发送一条通知。
(2) 系统也提供了一些并发队列(concurrent queue),咱们成为 Global Dispatch Queues。目前有四种全局队列,他们分别有不一样的优先级:background, low, default, high。须要注意的时,苹果的API也会使用这些队列,因此这些队列不会被咱们添加的任务独占。
(3) 咱们也能够建立本身的自定义串行队列或者并发队列。这意味着失少有五个(种)队列能够由咱们使用:main queue, 四种 global dispatch queue, 还有咱们自定义的队列。

注意:GCD 的重点在于,咱们要选择正确的队列调度功能(queue dispatching function),来把咱们的任务提交到队列中。

使用 dispatch_async 处理后台任务

原始代码:

UIImage *overlayImage = [self faceOverlayImageFromImage:_image];
[self fadeInNewImage:overlayImage];

修改成:

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
        });
    });
}
  • 注释1:首先咱们将主线程的任务移到了一个全局队列中。由于这里是 dispatch_async,这意味着这个代码块(block)被异步提交(submitted)。这样就会使得 viewDidload 更快执行完成。面部识别的代码就会稍晚被执行完(面部识别的代码由于由GCD控制,咱们无得知到底何时可以执行完成)。

  • 注释2:此时人脸识别所在的 block 已经执行完成,咱们生成了一个新的 UIImage。由于咱们须要用获得的 UIImage 来更新UIImageView,因此咱们在主线程上提交了一个 block。
    注意:全部UIKit 相关的操做都应该在主线程上执行。

  • 注释3:更新 UI

dispatch_async 中不一样队列的使用场景

  1. 自定义串联队列(custom serial queue)
    当你但愿在后台按顺序执行一些任务,而且追踪这些任务。这种方式消除了资源争夺现象。

  2. 主队列(Main Queue)
    当咱们完成了一个并发队列中的某个工做后,须要更新 UI,咱们一般使用 main queue。实现main queue 须要将一个 block 写在 另外一个 block 中。还有,若是咱们已经在主队列(main queue)中,而后还调用 dispatch_async 指向main queue,咱们就会获得保证——新的 task 在当前方法完成后必定会被执行。

  3. 并发队列
    一般非UI展现/更新的代码,都用并发队列。

使用 dispatch_after 实现延迟

下面代码的目的是,在viewDidAppear 后,根据状况为用户显示一个提示(为了引发用户的注意,这个提示延迟 1s出现)

-(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));  
    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];
        }
    });
}
  • 注释1:咱们声明了须要被 delay 的时间。

  • 注释2:咱们在等待了 delay 的这段时间后,异步的将这个block 添加到主线程中。

dispatchafter 其实至关于一个延时的 dispatchasync。一样咱们仍然不能控制 block 中执行的时间,当 dispatch_after 已经返回,咱们也没法取消执行 block 中的任务。

dispatch_after 的使用场景:

  1. Custom Serial Queue: 要当心将 dispatch_after 使用在一个 custom serial queue 上。通常仍是用在 main queue 上。

  2. Main queue :一般 dispatchafter 都用在 mainqueue 上

  3. Concurrent queue : 也要当心使用!

让咱们本身的单例实现线程安全(thread-safe)

关于单例,咱们一般都会关心——单例不是线程安全的。基于单例的使用状况,这个关心是很合理的:单例的 instance 常常被多个 ViewController 同时访问。

接下来,咱们在一个单例(singleton instance)本身制造一个竞争状况(race condition)。

本来代码以下:

+(instancetype)sharedManager   
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}

上面的代码很是简单,建立并初始化了一个单例。

须要注意的是,上面的 if 语句不是线程安全的。若是咱们屡次调用这个方法,有可能某一个线(Thread A)程会在 sharedPhotoManager 初始化以前就进入 if 语句中,而且 switch 在此时也未能出现。而后,另外一个线程(Thread B)进入 if 语句,初始化这个单例,而后退出。

当系统 context switch 切换到 Thread A ,咱们会再次初始化这个单例,而后退出。这样咱们就有了两个单例。这是咱们不但愿发生的。

咱们能够改变上面的代码,迫使这种极端状况发生以便咱们观察。

PhotoManager.m 中

+(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 的 sleep 方法,强制使 context switch 出现。

AppDelegate.m 中

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];
});

这里建立了多个异步并发,实例化单例,而后制造了一个 race condition。

咱们能够看到输出结果:

图片描述

能够看到,Log 出三个单例内存地址在这里是不一样的。

问题的缘由是,初始化PhotoManager 这部分做为一个 critical section,应该仅被执行一次,可实际却被执行了屡次。虽然上面的状况是咱们强制出现的,可是仍是有可能不当心形成这个问题。

注意:刚刚在运行中,三次编译两次都直接 crash,仅仅上面一次log 出上面的结果。这个问题的缘由是:系统事件是不受咱们控制的。若是出现了线程问题,由于问题难以重现,因此也很是难Debug。

为了纠正这个问题,实例化的代码应该仅被执行一次,由于 if 语句中的代码做为一个 critical section。这里就引出了咱们要使用的 dispatch_once。

将上面初始化的代码替换为

+(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;
}

能够看到,以前的问题已经没有再出现了。咱们把上面的代码调整一下

+(instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}

dispatchonce 在一种线程安全的模式下,执行而且只执行一个 block。在有一个线程已经在这个 critical section,若是有其余线程尝试访问 dispatchonce 中的 section,这些线程会一直被 block 直到 critical section 执行完成。

须要注意的是,这个作法实现了实例的线程安全,并无实现类的线程安全。若是须要,在类中还须要添加其余的 critical block,例如咱们须要操做重要的内部数据的时候。这些咱们可使用其余的方法来实现线程安全,例如 同步一个数据的访问(synchronising access to data),咱们在后面会提到。

处理读写问题(Reader and Writer Problem)

实例的线程安全并不只仅是单例要处理的惟一问题。若是单例属性是一个 mutable 对象,咱们就须要考虑这个这个对象是不是线程安全的。关于对象是不是线程安全,咱们能够参照苹果提供的一份文档:点击连接
举例来讲,NSMutableArray 就不是线程安全的。

虽然不少线程均可以从 NSMutableArray 的实例当即访问,可是当它正在某个线程被访问的时候还让其余线程来访问显然是不安全的。咱们并无防止这种状况的发生。

仍是 PhotoManager.m 中

-(void)addPhoto:(Photo *)photo
{
    if (photo) {
        [_photosArray addObject:photo];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self postContentAddedNotification];
        });
    }
}

上面是一个 write 方法,给 photoArray 添加一个对象。

-(NSArray *)photos
{
  return [NSArray arrayWithArray:_photosArray];
}

这里是一个 read 方法。为了防止调用者改变这个数组,它给调用者生成了一个 photoArray 的 immutable 的copy。但咱们仍须要注意,当一个线程调用 read 方法时,要防止另外一个线程调用 write 方法。这个问题是软件开发的经典问题 Reader-Writer Problem。GCG经过使用 dispatch barriers ,制造了一个 Reader-Writer lock 提供了一个很是好的解决方式。

Dispatch barrier 的做用是,在并发队列中,提供一种相似串行的瓶颈。使用 dispatch barrier 能够确保被提交的 block 在一个特定时间段内被这个 queue 惟一执行。这意味着,全部以前提交到队列中的 block 必需要在 barrier block 执行前执行完成。

当轮到这个特定的block 的时候,这个 barrier 会执行这个 block,并确保这个 queue 在这个block 执行期间不执行其余的 block。一旦完成,这个queue 就会按原来的设定继续执行。GCD 提供了同步和异步两种 barrier 功能。

图大体说明了 barrier 功能的效果。

Dispatch-Barrier.png

能够看到队列中的通常操做和普通的并发队列并无太大区别,可是当执行到barrier block 的时候,这个队列就开始向一个 串行队列了。当这个 barrier block 执行完成以后,这个队列又开始恢复到并发队列。

barrier 的使用场景

Custom serial queue:不要在这里使用 barrier,这已是一个 serial queue,使用 barrier 毫无心义。
Global Concurrent Queue:在这里要当心使用,由于其余系统操做可能会使用这个队列,因此使用 barrier 将 Global Concurrent Queue 独占存在风险。
Custom Concurrent Queue:放心使用;

如上面所说,由于惟一比较好的 barrier 使用方式是在 custom concurrent Queue 中使用,因此咱们须要本身建立一个并发队列来处理 barrier ,以便将 read 和 write 方法独立开来。

在 PhotoManager.m 中,添加一个 dispatch 的 property:

@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:检查 photo 是否存在

  • 注释2:用咱们的自定义 queue 添加写操做,当 critical section 在稍后执行的时候,这将是队列中惟一被执行的 task。

  • 注释3:将对象添加到数组。由于这是一个 barrier block,不会有其余的 block 同时和这个block 在 concurrentPhotoQueue 中运行。

  • 注释4:最后发送一个通知,广播图片已经添加完成。由于这个通知包含 UI 相关的代码,因此须要从 main thread 中发出,因此在这里咱们调度另外一个异步任务到主线程,以完成发送通知。

上面的代码处理了写方法,咱们还须要实现 photos 的读方法,还有对 concurrentPhotoQueue 实例化。
为了保证写方法那边的线程安全,在读方法这边咱们也须要作一些处理。虽然咱们须要从 photos 方法得到返回值,但不能异步调度到这个线程,由于它不必定会photo 方法 return以前结束完成。
这种状况,dispatch_sync 就是很是好的选择。

dispatchsync 以同步方式将代码块提交,而且等待 block 完成才返回。咱们可使用 dispatchsync 来追踪 dispatch barrier block,或者当咱们须要等待某个操做结束,以获得这个 block 处理后的数据。若是是第二种状况,咱们一般会看到一个 _block 变量写在dispatchsync 的外面,以便使用被 dispatch_sync 中的block 处理后的数据。

可是,使用 dispatchsync 的时候仍是要至关当心。若是咱们吧 dispatchsync 使用在咱们当前正在运行的队列上,会致使死锁,由于这个调用会等到 block 结束后执行,可是这个 block 不能结束(由于都不能开始),由于当前的任务也没有结束。因此咱们必需要当心咱们要从哪一个队列调用,也要当心将哪一个队列传入。

dispatch_sync 的使用场景

  • Custom Serial Queue:必定要很是当心; 若是咱们已经在一个队列中运行,而后调用了 dispatch_sync 指向当前这个队列,会形成死锁。

  • Main Queue:一样要很是当心;和上面缘由同样,要注意死锁。

  • Concurrent Queue:通常都放心使用;无论是和 dispatch_barrier 协同使用,仍是等待一个任务结束以便下一步处理,都是合理的选择。

仍是在 PhotoManager.m 中,修改 photos 读方法

-(NSArray *)photos
{
    __block NSArray *array; // 1
    dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
        array = [NSArray arrayWithArray:_photosArray]; // 3
    });
    return array;
}
  • 注释1:__block 关键字容许 block 中的对象是 mutable 的。若是没有这个关键字, block 中的 array将会变为只读,代码将会没法编译。

  • 注释2:同步调度到 concurrentPhotoQueue 来进行读方法。

  • 注释3:存储 photo 数组并返回。

注意:若是咱们但愿在一个 block 外面声明一个对象,在 block 内给它赋值,咱们就须要在声明的时候加上 __block 关键字,这个关键字容许咱们修改 block 内的对象。不然将没法编译。

最后咱们须要实例化 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;
}

上面的代码使用 dispatchqueuecreate 将concurrentPhotoQueue 初始化为一个并发队列。这一个参数通常写成反 DNS 形式,最好确保有必定意义,以便 Debug。第二个参数指定咱们但愿队列是串行的仍是并发的。
注意:上面的 dispatchqueuecreate 方法中,咱们能够看到常常有人传 0 或 NULL 做为第二个参数。可是这种作法是过期的,最好仍是说明咱们的参数。

如今,PhotoManager 单例已是线程安全的了。无论咱们怎么读写这些图片,咱们均可以确保这些都会以相对安全的方式完成。

Dispatch Groups

咱们先来看下面的示例代码:

-(void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
    __block NSError *error;
 
    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;
        }
 
        Photo *photo = [[Photo alloc] initwithURL:url
                              withCompletionBlock:^(UIImage *image, NSError *_error) {
                                  if (_error) {
                                      error = _error;
                                  }
                              }];
 
        [[PhotoManager sharedManager] addPhoto:photo];
    }
 
    if (completionBlock) {
        completionBlock(error);
    }
}

在这个方法的末尾,调用了 completionBlock 。实际这样写是有问题的,并不能保证上面的下载图片方法可以当即完成。由于 initwithURL : withCompletionBlock : 这个方法是异步方法,因此下载尚未完成,这个方法就返回了。可是 downloadPhotosWithCompletionBlock 这个方法却以同步的方式,在方法最后调用了 completion block

正确的作法应该是:downloadPhotosWithCompletionBlock 仅应该在图片下载的异步方法initwithURL : withCompletionBlock :完成并调用了它的 completion blockdownloadPhotosWithCompletionBlock 再去调用本身的 completion block

可是,问题来了——下载图片的方法是异步方法,前文章里咱们已经说过,异步任务虽然也是按照提交(submit)的顺序开始的,可是一旦开始执行,咱们就没法控制这个任务何时被执行完成——这些都由 GCD 来控制。

也许咱们会想到使用一些全局的 bool 型变量来记录下载的状态,这种作法不是说不行,但的确是比较笨的办法。
幸运的是 GCD 为咱们提供了 dispatch groups,使用 dispatch groups 来监控异步任务的完成状况在合适不过了。

Dispatch groups 会在一组任务完成以后通知咱们。这些任务能够是同步的也能够是异步的,甚至这些任务不在同一个队列里也可以被追踪(监控)。当一组任务完成以后,dispatch groups 也能够按同步或异步的方式来通知咱们。由于不一样队列的任务也能够被追踪,须要有一个 dispatchgroupt 的实例来追踪不一样队列中的不一样任务。

GCD 提供了两种方式来实现追踪

1. dispatchgroupwait

这个功能用来阻塞当前的线程,一直到这个任务组(group)的全部任务结束,或者直到一个 timeout 出现。dispatch_group_wait是一个同步功能。咱们能够将这个功能应用在以前有问题的代码中:

-(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:由于咱们使用了同步功能 dispatchgroupwait,它阻塞了当前的线程。咱们将这整段代码都包含在了一个 dispatchasync 当中,就是为了不这个主线程在这里不被 dispatchgroup_wait 所阻塞。

  • 注释2:建立一个新的 dispatch group,它的做用有点像一个用来记录未完成的任务的计数器。

  • 注释3:dispatchgroupenter 用来通知一组任务开始了。在使用中须要注意的是,咱们使用了多少个 dispatchgroupenter ,一般就要使用多少个 dispatchgroupleave , 不然可能会形成一些很是奇怪的 bug。

  • 注释4:通知这一组(group)任务已经完成了。

  • 注释5:dispatchgroupwait ,开始等待,一直到这组任务所有结束,或者知道任务出现了 timeout。若是发生了 timeout,dispatchgroupwait 会返回一个非零结果。咱们能够用这个结果来判断任务是否是超时。可是,若是咱们向上面的代码写的同样,使用了 DISPATCHTIMEFOREVER ,这个任务就永远不会超时,dispatchgroupwait 就会一直等待直到任务完成。

  • 注释6:此时咱们就能知道,全部的图片下载任务要么是下载完成了,或者是超时了。而后咱们就能回调到主线程,调用 downloadPhotosWithCompletionBlock 这个方法的 completion block。

  • 注释7:检查 completion block 是否为空,若是不为空,返回。

上面的代码如今看起来效果还不错,可是咱们最好仍是要避免阻塞线程的操做(同步方式)。接下来咱们会重写上面的方法,当下载完成的时候,使用异步通知的方式,使咱们可以知道下载结束了。

咱们先来了解一下 dispatch group 的使用场景:

  1. 自定义穿行队列(Custom serial queue):比较适合使用通知形式。

  2. 主队列(Main queue):可使用但必定要当心,有可能咱们提交的任务会阻塞主线程。

  3. 并发队列(Concurrent queue):无论是使用通知形式仍是 dispatchgroupwait, 都是很好的选择。

接下来咱们介绍 dispatch groups 的另外一种使用方式

上面的代码的确实现咱们须要的效果了,可是咱们能够看到,咱们必须 dispatch async 到另外一个队列而后才能使用 dispatchgroupwait 。咱们能够尝试使用另外一种方法,修改上面的代码:

-(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:在这里咱们就不用像使用 dispatchgroupwait 时候那样把整个方法都放在 dispatch async 里,由于咱们在这里并无阻塞主线程。

  • 注释2:和上面同样,表示一个 block 进入了这个group。

  • 注释3:一个 block 离开了 group。

  • 注释4:这段代码会在以前的 dispatch group 中所有执行完时执行。以后咱们就须要执行 completion block 了,在这里咱们要制定咱们以后但愿在那个队列来执行 completion block,在这里就是 main queue 了。

关于

void dispatch_group_notify ( dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block );

做用是:当以前提交到 group 的任务(block)完成时,把一个 block 提交到一个队列。

参数1:咱们要追踪的 dispatch group
参数2:当 group 完成以后,咱们用来提交 block 的队列。
参数3:要被提交的 block。

大量使用并发的风险

咱们来看一下上面的代码,方法中有一个 for 循环,用来下载三张不一样的图片。咱们能够尝试一下可否对这个 for 循环使用并发,加速下载。

在这里咱们就能够尝试使用 dispatch_apply。

dispatchapply 的做用有点像一个用来并发执行不一样的迭代(iterations)的for 循环。dispatchapply 这个方法是同步的,因此和通常的 for 循环同样,dispatch_apply 只会在执行完成以后才会返回。

须要注意的是,dispatchapply 中的迭代数量也不宜太多,过多的迭代可能会形成每一个迭代的碎片时间、消耗累积,影响使用。在这里 dispatchapply 中的迭代总数须要咱们在编码过程当中尝试、优化。

在哪里使用 dispatch_apply 比较合适呢?

自定义串行队列(Custom serial queue):不适用,由于 dispatch_apply 是同步方法,仍是老老实实使用 for 循环把。

主队列(Main queue):跟上面同样,一样不适用。
并发队列(Concurrent queue):适用 dispatch_apply,特别是当咱们须要追踪任务完成状况。

咱们再次修改上面的代码:

-(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);
        }
    });
}

修改后,以前的 for 循环就能够并发执行了。在上面的调用 dispatch_apply 时,第一个参数是迭代的数量,第二个参数是真正执行任务的队列,第三个就是要提交的 block。

须要注意的是,咱们已经把 add photo 的方法修改成线程安全,可是获得图片的顺序仍是不必定的,由于这取决于 dispatch_apply 中哪个迭代先执行完成。

运行后咱们会发现,屡次点击从网络加载图片,偶尔下载速度会有一点提高。

但事实上,使用 dispatch_apply 来实现这个效果并不太值得,缘由是:

  1. 由于线程并行的缘由,多多少少会增长运行开销。

  2. 在这里咱们也能体会到,使用 dispatch_apply 替换 for 循环之后,效果并无太大提高。因此在这里花时间显得有些不太值得。

  3. 一般来讲,代码优化会使得代码变得更复杂,也让别人更难读懂咱们的代码。因此优化前要考虑到这一点。

GCD 的其余功能

除了上面介绍过的这些经常使用功能外,GCD 还有一些不是那么经常使用的功能,但在某些场合使用也许会十分方便。

在 Xcode 中测试是在 XCTestCase 的一个子类上执行。测试方法的方法名一般是本来的方法签名前加一个 test。测试是在主线程上进行的。

一旦一个测试方法运行完成,XCTest 方法会认为这个方法已经完成而且运行下一个 test。这就意味着,若是一个方法中包含异步代码,颇有可能在运行下一个方法的时候,当前方法还在继续运行。

一般网络请求相关代码都是异步的,加上 test 一般都会在测试方法执行完成后就结束,这会使得网络请求代码很难测试。可是,咱们能够在 test 方法中阻塞主线程一直到网络请求完成。

这里须要注意的是,是否使用这种方式进行测试你们仍是有分歧的,有些人认为这种方式没有很好的遵循集成测试的设定。可是若是这种方式对咱们有用,不妨一试。

在 test 方法中,本来代码以下:

-(void)downloadImageURLWithString:(NSString *)URLString
{
    NSURL *url = [NSURL URLWithString:URLString];
    __block BOOL isFinishedDownloading = NO;
    __unused Photo *photo = [[Photo alloc]
                             initwithURL:url
                             withCompletionBlock:^(UIImage *image, NSError *error) {
                                 if (error) {
                                     XCTFail(@"%@ failed. %@", URLString, error);
                                 }
                                 isFinishedDownloading = YES;
                             }];
 
    while (!isFinishedDownloading) {}
}

用上面的方法测试并非很合理。方法末尾的 while 循环等待 isFinishedDownloading 在 completion block 中变为true。运行 test,为了让现象更加明显咱们能够开启手机开发者模式里的网络控制,把网络状态调整为 very bad。打开 debug navigation, 在我这里,能够很是明显的看到 CPU 使用率保持在 95% 高居不下,内存使用也从 6点几上涨到了 8 点几。

这种实现方法叫作自旋锁。形成这个现象的缘由是,while 循环在网络请求结束以前,不间断的检查 isFinishedDownloading 中的值。

Semaphores(信号灯)

信号灯是一个很是 old-school 的概念。若是想了解更多一些,能够百度“哲学家就餐问题”。
信号灯容许咱们控制多个消费者对有限的资源的访问。咱们能够把信号灯想象成一个餐厅的服务员,咱们的餐厅只有两个位子,那么每次咱们最多就只能容许两位客人进去用餐,那么剩下的客人就要排队等位子用餐(First in first out)。

咱们来尝试使用 semaphores,将上面的代码替换为:

-(void)downloadImageURLWithString:(NSString *)URLString
{
    // 1
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
 
    NSURL *url = [NSURL URLWithString:URLString];
    __unused Photo *photo = [[Photo alloc]
                             initwithURL:url
                             withCompletionBlock:^(UIImage *image, NSError *error) {
                                 if (error) {
                                     XCTFail(@"%@ failed. %@", URLString, error);
                                 }
 
                                 // 2
                                 dispatch_semaphore_signal(semaphore);
                             }];
 
    // 3
    dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, kDefaultTimeoutLengthInNanoSeconds);
    if (dispatch_semaphore_wait(semaphore, timeoutTime)) {
        XCTFail(@"%@ timed out", URLString);
    }
}

咱们来看看这段代码的做用。

  • 注释1:建立 semaphore,参数表示 semaphore 初始的值。这个数字表示能够直接访问信号灯,而不用先让信号灯增加的对象数量。(让一个信号灯增加就是给一个信号灯发信号)

  • 注释2:在这里咱们通知这个信号灯,咱们不在须要这个资源了。这里让信号灯增加,而且代表,信号灯能够接受其余对象的访问了。

  • 注释3:等待信号灯发信号,并设置一个 timeout 的时间。这里会阻塞当前的线程,知道 semaphore 被通知。若是超时会返回一个非零的结果(non-zero result)。

再次运行 test 能够看到,CPU 使用率一直是 0,在十秒后返回了一个超时错误。

Dispatch sources

Dispatch source 简单说是一个一些 low-level 功能的集合,能够帮助咱们监控 Unix 信号、文件描述、VFS 节点等不太经常使用的东西。上面说的这些东西我本身也没怎么用过,在文中就不作介绍了,咱们能够尝试一种特殊的方式来使用如下 dispatch source。
咱们来看一下关于 dispatch source 的建立方法:

dispatch_source_t dispatch_source_create(
   dispatch_source_type_t type,
   uintptr_t handle,
   unsigned long mask,
   dispatch_queue_t queue);

关于这个方法的文档

第一个参数 dispatchsourcetype_t,这是最重要的参数。关于这个方法在官方文档的详细介绍:建立一个新的 dispatch source 来监控 low-level 的系统对象,而且自动注册一个 handle block 到 dispatch queue 来对事件做出回应。

参数 type:dispatch source 的类型,必须是这个列表中常量。
参数 handle:用来进行监控。这个参数取决于上面的 type 参数。
参数 mask:一个标志的 mask,用来肯定须要哪些事件。一样也受 type 的指挥。
参数 queue:事件操做 block 要被提交到的 queue。

返回值是一个 dispatch source 对象,若是没有建立成功的话返回为空。

这里要监听的是一个 DISPATCHSOURCETYPE_SIGNAL。

dispatch source 会监听当前的进程,等待信号。handle 是一个int 类型的信号数字。Mask 暂时没有用到,先传0。

在这里咱们能够看到一个 Unix signal 列表,在这里咱们会监听 SIGSTOP 信号。这个信号会在进程收到了一个不可避免的暂停指令后发出。当咱们使用 LLDB debugger 来 debug 咱们的 APP 的时候,其实也是发送了这个信号。

咱们在 PhotoCollectionViewController.m 添加下面的代码:

-(void)viewDidLoad
{
  [super viewDidLoad];
 
  // 1
  #if DEBUG
      // 2
      dispatch_queue_t queue = dispatch_get_main_queue();
 
      // 3
      static dispatch_source_t source = nil;
 
      // 4
      __typeof(self) __weak weakSelf = self;
 
      // 5
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
          // 6
          source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGSTOP, 0, queue);
 
          // 7
          if (source)
          {
              // 8
              dispatch_source_set_event_handler(source, ^{
                  // 9
                  NSLog(@"Hi, I am: %@", weakSelf);
              });
              dispatch_resume(source); // 10
          }
      });
  #endif
 
  // The other stuff

咱们来一步步看一下上面代码的做用:

  • 注释1:这段代码最好是在 Debug 模式下使用。

  • 注释2:声明 queue,注意这里的 queue 就是主队列。

  • 注释3:声明 dispatch_source。

  • 注释4:使用 weakSelf 是为了保证没有 retain 循环。在这里使用 weakSelf 也不是必定须要的,由于 PhotoCollectionViewController 这个类在 App 的整个声明周期都会存在。可是,若是咱们在使用过程当中有一些类(View Controller)会disappear,weakSelf 任然能保证没有 retain 循环。

  • 注释5:使用 dispatch_once ,让 dispatch source 只执行一次。

  • 注释6:给 source 赋值,参考上面咱们对 dispatchsourcecreate 方法的介绍。第二个参数表示,咱们要监听 SIGSTOP 信号。

  • 注释7:若是使用了不正确的参数,dispatchsourcecreate 是不能执行成功的。因此咱们在这里进行检查。

  • 注释8:dispatchsourceseteventhandler 会在咱们收到监听的信号时调用。

  • 注释10:默认状况下,全部的 dispatch sources 是暂停状态的。咱们在这里让 source 恢复,以便开始监控其余事件。

    如今运行程序,而后在 Debugger 中暂停,而后再继续运行。

咱们能看到在点击继续时候,控制台 log 了上面的文字。如今咱们的代码可以检查到进入了 debug 状态。

咱们能够用这个方法来 debug 对象,并在 resume 的时候显示数据。还有一个有趣的使用方法:咱们能够用它来作堆栈追踪工具,在 debugger 里面找到咱们想要操做的对象。

使用这个方法咱们能够随时中止 debugger,而后让代码在咱们要执行的位置执行。

例如,咱们能够在上面代码 NSLog 位置设置一个断点,暂停一下,而后继续,应用就会触发咱们刚刚添加的断点。在这里咱们就能够访问当前这个类的变量了。

如今在 debugger 中输入

po [[weakSelf navigationItem] setPrompt:@"HAHAH!"]

能够看到咱们在没有改变代码的前提下,Navigation Bar 上的文字被改变了。

使用这种方法,咱们能够直接更新当前 UI,查询当前类的值,甚至执行一个方法,restart 应用。

图片描述

Bingo!

有须要的话能够点击这里查看完成后的Demo。
参考连接

相关文章
相关标签/搜索