GCD ( 二 ) :为主线程加载提速,创建线程安全的单例

2019-05-25  本文已影响0人  司空123

在这篇文章里我们将通过具体的实例,来描述GCD的使用情况,以及需要注意的地方,这或许可以帮助你更好的理解GCD.

一、体会异步带来的好处:

有一个场景:在控制器中需要,我们需要渲染并加载一张大图getImageForSomeTime,这个方法会耗费一些时间,我们在viewDidLoad中初始化一些UI代码,然后调用getImageForSomeTime获取渲染好的图片,最后加载到页面上.代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.photoImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 44, self.view.bounds.size.width, 400)];
    self.photoImageView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.0];
    [self.view addSubview:self.photoImageView];
    
    UIImage *image = [self getImageForSomeTime:4.0f];
    self.photoImageView.image = image;
}

当我们启动项目,进入到这个页面的时候花了很长时间,能感到明显的滞后,在重载 UIViewController 的 viewDidLoad 时容易加入太多杂乱的工作,这通常会引起视图控制器出现前更长的等待。如果可能,最好是卸下一些工作放到后台执行,如果它们不是必须要在当前线程中加载。
用下面的实现替换 viewDidLoad :

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.photoImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 44, self.view.bounds.size.width, 400)];
    self.photoImageView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.0];
    [self.view addSubview:self.photoImageView];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1
        UIImage *image = [self getImageForSomeTime:4.0f];
        dispatch_async(dispatch_get_main_queue(), ^{ // 2
            self.photoImageView.image = image;//3
        });
    });
}

下面来说明上面的新代码所做的事:

1.主线程执行到: viewDidLoad
2.实例化了一个UIImageView并加载到self.view上
3.主线程目前在 viewDidLoad 内,正要到达 dispatch_async
4.dispatch_async 将工作从主线程移到全局线程,Block 会被异步地提交,并在稍后执行。
5.viewDidLoad在添加dispatch_async到全局队列后回到主线程继续进行,把注意力转向剩下的任务。同时,全局并发队列正在处理异步提交的Block。记住 Block 在全局队列中将按照FIFO 顺序出列,但可以并发执行。
6.这就使得 viewDidLoad 更早地在主线程完成,主线程中其他任务的执行时间也得以提前,这就让加载过程感觉起来更加快速。
7.接下来,图片渲染过程完成,并生成了一个新的图像。既然你要使用此新图像更新你的 UIImageView ,那么你就添加一个新的 Block 到主线程。记住——你必须总是在主线程访问 UIKit 的类。
8.最后,调用 self.photoImageView.image = image回主线程更新 UI 。

编译并运行应用,发现视图控制器加载明显变快,除图片加载外的任务可以被更早的执行。这给应用带来了不错的效果,和之前的显示差别巨大。

二、让你的单例线程安全:

对于单例,一直有一个担忧,那就是他的线程安全问题,这个担忧十分合理,因为单例常常被多个控制器同时访问。

单例的线程安全问题来自两个方面:
1.单例的初始化过程:
2.属性的读写过程:

单例的实例化代码如下: 这是一个线程不安全的单例

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

当前状态下,代码相当简单;你创建了一个单例并初始化一个叫做 photosArrayNSMutableArray 属性。

然而,if 条件分支不是线程安全的;如果我们在多个线程同时调用这个方法,有一个可能性是在某个线程(线程1)上进入 if 语句块并且在 sharedPhotoManager 被分配内存前发生一个上下文切换;然后另一个线程(线程2)也进入 if条件分支,此时(!sharedPhotoManager)为真,那么系统会为此单例实例分配内存,然后退出。

接着,系统上下文切换回线程1,系统会分配另外一个单例实例的内存,然后退出。在那个时间点,你有了两个单例的实例——很明显这不是你想要的.

当然,发生这样的事情概率很低,因为满足这样的条件必须是上下文切换刚好发生在if分支语句处,但,这毕竟是一个风险.

在上面的代码中用 NSThread 的 sleepForTimeInterval: 类方法来强制发生一个上下文切换。

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"sharedManager address: %p", sharedPhotoManager);
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}

然后在控制器中,创建两个异步并发调用来实例化PhotoManager单例,引发上面描述的竞态条件。

- (void)viewDidLoad {
    [super viewDidLoad];
  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];
    });

编译并运行项目;查看控制台输出,你会看到多个单例被实例化,如下所示:


屏幕快照 2019-05-17 下午4.13.01.png

注意到这里有好几行显示着不同地址的单例实例。这明显违背了单例的目的,对吧?

这个输出向你展示了临界区被执行多次,而它只应该执行一次。现在,固然是你自己强制这样的状况发生,但你可以想像一下这个状况会怎样在无意间发生。

要纠正这个状况,实例化代码应该只执行一次,并阻塞其它实例在 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(@"sharedManager address: %p", sharedPhotoManager);
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}

编译并运行应用;查看控制台输出,你会看到有且仅有一个单例的实例——这就是你对单例的期望!

dispatch_once() 以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。

需要记住的是,这只是让单例的实例线程安全。它绝对没有让类本身线程安全。类中可能还有其它竞态条件,例如任何操纵内部数据的情况。这些需要用其它方式来保证线程安全,例如上面提到的信息读写过程:.

三、属性读写过程的线程安全:

实例化时的线程安全不是处理单例时的唯一问题。如果单例的属性表示一个可变对象,那么你就需要考虑是否那个对象自身线程安全。

如果问题中的这个对象是一个 Foundation 容器类,那么答案是——“很可能不安全”!众多的 Foundation 类都不是线程安全的。 NSMutableArray就是其中一个:
要分析这个问题,看看 PhotoManager.m 中的 addPhoto:方法

- (void)addPhoto:(UIImage *)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 在那个特定时间上是指定队列上唯一被执行的条目。这就意味着所有的先于障碍(barrier)提交到队列的条目必能在这个 Block 执行前完成,所有后于障碍(barrier)提交到队列的条目在这个Block后面完成。

当这个 Block 的时机到达,调度障碍执行这个 Block 并确保在那个时间里队列不会执行任何其它 Block 。一旦完成,队列就返回到它默认的实现状态。 GCD 提供了同步和异步两种障碍函数。

下图显示了障碍函数对多个异步队列的影响:

Dispatch barriers.png

注意到正常部分的操作就如同一个正常的并发队列。但当障碍执行时,它本质上就如同一个串行队列。也就是,障碍是唯一在执行的事物。在障碍完成后,队列回到一个正常并发队列的样子。

Dispatch barriers的使用指导:

由于上面唯一像样的选择是自定义并发队列,你将创建一个你自己的队列去处理你的障碍函数并分开读和写函数。且这个并发队列将允许多个多操作同时进行。

打开 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 不能完成(它甚至不会开始!),直到当前已经存在的任务完成,而当前任务无法完成!这将迫使你自觉于你正从哪个队列调用——以及你正在传递进入哪个队列。

继续在 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.singleton.photoQueue",DISPATCH_QUEUE_CONCURRENT);
    });
    return sharedPhotoManager;
}

恭喜——你的 PhotoManager 单例现在是线程安全的了。不论你在何处或怎样读或写你的照片,你都有这样的自信,即它将以安全的方式完成,不会出现任何惊吓。

参考@nixzhu在Github上的译文,
https://github.com/nixzhu/dev-blog/blob/master/2014-04-19-grand-central-dispatch-in-depth-part-1.md

原作者:Derek Selander

原文地:[http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1]

上一篇下一篇

猜你喜欢

热点阅读