Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

深入学习中央调度(GCD)--第一部分

$
0
0
     更新说明:查阅我们基于iOS8.0和Swift下中央调度(https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1)教程这块的更新版本。
尽管中央调用(简称GCD)已经存在一段时间了,但并不是每个人都知道如何有效地使用它。这是可以理解的,并发本身就是棘手的,然而基于C语言的GCD API看起来像一套深入OC世界的弯角(转换器)。这个系列教程分两部分,深入地介绍中央调度(GCD)。 在这两部分中,第一部分解释了什么是GCD以及GCD常用的几个基本函数,在第二部分中,将会介绍几个GCD提供的更高级的功能。 **

什么是GCD?

** 
libdispatch俗称GCD,苹果提供的库,用以支持在iOS和OS X的多核硬件上执行并行代码。它有以下几个有点: 
1、GCD可以通过延缓耗时的计算任务放在后台运行来提高App的响应能力 
2、GCD提供了比加锁和线程更加简单的并发模型来避免并发bugs 
3、GCD可以使用高性能的执行单元优化代码,比如常用的模式:单例 
本教程假设你已经对blocks和GCD有一个基本的了解,如果是全新接触GCD,可以查阅供初学者;了解学习要点的基于iOS的多线程处理和中央调度。

 **

GCD术语

** 
要理解GCD,需要能应付自如几个跟线程和并发相关的概念。这些可能既模糊又微妙,所以在GCD的上下文中花点时间去简要回顾一下它们。 
1、串行与并行 
这俩术语描述了任务被执行时彼此的关系。串行执行任务每次执行一个任务,并发执行任务可能在同一时间执行多个任务。 
尽管这些术语有广泛的应用,但对于该教程来说,你可以把一个任务当做是一个OC代码块。不知道什么是块(block)?请查阅在iOS5下如何使用blocks。实际上,你也可以以函数指针的方式使用GCD,但在大多数情况下这样使用起来更加棘手。Blocks是更加简单的。 
2、同步与异步 
在GCD下,这俩术语描述了当一个功能完成之后与之关联的另一个任务功能如何请求GCD调用执行。同步意味着仅当任务按序执行完毕之后才会返回。 
异步,换句话说,就是立即返回预定的任务要执行但是不会等待。因此,异步不会阻塞当前线程的执行继续向下执行。 
注意,当你看到一个阻塞当前线程、函数或操作的的同步操作时,不要弄混了。这个动作块描述了一个功能是如何影响它的线程,并且没有连接到名词块(描述了一个在OC中的字面匿名函数且定义了一个提交到GCD的任务)。 
3、临界区 
这是一段不能被并发执行的代码,那就是,同时只能有一个线程执行。这就是一般并行进程访问共享资源(比如变量)的代码变坏的原因。 
4、争用条件 
这种情况是由软件系统依赖一个特殊的序列或在一个不受控制的事件(如:程序的并发任务的确切执行顺序)执行时间下产生的。争用情况可能产生不可预期的行为,而且不是立即可以通过代码检查就能发现的。 
5、死锁 
在大多数情况下,两个(有时更多)元素被说成是线程死锁是因为他们陷入了彼此等待而不能正常的完成或执行其他行为。一个不能结束是因为正在等待另一个结束。另一个不能完成是以为在等待第一个结束。 
6、线程安全 
线程安全的代码可以安全地被多个线程或并发任务调用而不会引起任何问题(如:数据异常、崩溃等)。线程不安全的代码同一时间下仅仅可以在一个上下文中运行。一个线程安全的例子就是不可变字典,你可以在多个线程中同时使用而不会出问题。换句话说可变字典不是线程安全的,因为同一时间下,仅可以可以在一个线程中访问(安全而不出问题)。 
7、上下文切换 
上下文切换是指当一个程序存储和恢复执行状态(当你在单个进程中在不同线程间切换时)。这中程序在你写多任务程序时很常见,但是也带来了一些额外的开销作为代价。 
并发以并行 
并发和并行常常被同时提到,因此简要的说明下两者之间的区别还是值得的。 
分离的并发代码可以被“同时”执行。然而,这是由系统决定如何发生-或者如果完全发生的话。多核心得设备同时执行多个线程通过并行。然而在单核心设备中为了达到并发,运行一个线程时必须通过上下文切花来运行另一个线程。这通常发生的足够快,我们可以假想它按下图方式执行:

这里写图片描述 
尽管你可能会在GCD下写代码以使用并发执行,但最终是由GCD决定多少并行是必须的。并行必定并发,但是并发不能保证并行。 
这里更深层点的问题是,并发实际上是结构上的。当你在头脑中构思GCD代码时,就要规划代码结构以拆分为可同时运行的工作片和可以不必同时运行的任务。如果想更深入地研究这个问题,查阅这个精彩的演讲(this excellent talk by Rob Pike.)。 
队列 
GCD提供了调度队列以处理代码块,这些队列管理你提交到GCD的任务并按FIFO顺序执行。这保证了第一个进入队列的任务是第一个开始执行的,第二个添加到队列的将第二被执行,接下来按序。 
所有的调度队列对他们自己而言是线程安全的,可以在不同的线程中同时访问。GCD的优势是明显的(当你理解自己不同部分的代码是如何线程安全地访问调度队列时)。关键就是选择合适的调度队列类型和合适的dispatching函数提交自己的任务到队列。 
这节中,将看到两种类型的调度队列,GCD提供的特定队列以及通过一些列子说明如何使用GCD调度函数添加任务到调度队列。 
1、串行队列 
在串行队列中的任务一次执行一个,每个任务的开始必须是前面的任务完成之后。当然,也无需知道一个代码段何时结束及下一个何时开始,如图所示: 
这里写图片描述 
这些任务的执行时间是由GCD控制的,能知道的就是一次执行一个任务,按照添加到队列的顺序按序执行。 
由于在串行队列中不会有两个任务并发执行,也就没有同时访问临界区的并发问题,以保护临界区不会被争竞条件影响。因此,访问临界区的唯一方式就是通过提交到调度队列的任务访问,保证临界区安全。 
2、并发队列 
在并发队列中的任务仅仅能保证按照添加进的顺序启动,and这也是能保证的所有。元素可能一任何顺序结束,你也不能确定下一个block还要多长时间才能开始,同时在执行的blocks数目也不能确定,这都是GCD决定的。 
下面的图表展示了一个任务执行的示例,其中GCD控制了4个并发任务: 
这里写图片描述 
备注:现在block1,2和3运行很快,一个接一个。block1开始执行花费了一点时间在block差不多执行结束后才开始。同样的,在block2开始后block3也开始执行了但并不是block2结束后才开始。 
何时开始一个block执行完全由GCD决定。如果执行一个block的时间超时了,GCD会决定是否在另一个可用的核心上开始另一个任务或者切换上下文去执行另一个不同的 代码块。 
令人欣喜的是,GCD提供了至少5个特别的队列类型可供选择。 
3、队列类型 
首先,系统提供了一个特别的串行队列成为主队列。像任何串行队列一样,在这个队列中一次只能执行一个任务。然而,它可以保证所有的任务都在主线程(必须要保证所有更新UI的操作必须在这个线程执行)中执行。这个队列是一个用于接收UIView消息和通知的队列。 
该系统还提供了其他几个并发队列。这些统称为全局调度队列。有4个不同优先级的全局队列:background, low, default, high.值得一提的是,苹果的api也使用这些队列,因此你添加任何任务到这些队列,其中任务不只有你添加的。 
此外,你也可以创建你自定义的串行或并行队列。这意味着至少有5个队列任由你处置:主队列,4个全局队列,再加上任何一个你添加的自定义的队列。 
这就是调度队列的“伟大蓝图”。 
GCD的艺术来源于选择合适的队列去提交任务。最好的经验就是通过下面的例子学习,在哪里我们根据长期经验提供了一些一般性的建议。

开始

 由于本教程的目的是既要简单又要安全的从不同的线程调用代码,你将从头到尾完成这个GoodPuff项目。
      GoodPuff是一个非优化的,非线程安全的app。在这里你要瞪大眼睛去分辨COre image API的使用。对于基本的图片来说,你可以从相册库中选择也可以从一系列未知的图片url下载使用 。
      Download the project here.
      一旦下载好,提取到一个合适的位置,用Xcode打开并运行它,看起来会像下面一样:

这里写图片描述
注意:当你选择下载图片选项时,UIA了人VIew会过早的弹出,浙江在本系列的第二部分修复。 
在这个项目中使用了四个类: 
PhotoCollectionViewController:这是启动app后的第一个视图控制器。它以缩略图的形式展示所有选中的图片。 
PhotoDetailViewController:这个以大图的形式在UIScrollView中展示图片 
Photo:这是一个类聚合,其可以从NSURL或ALAsset创建图片。该类提供图片,缩略图或一个下载图片的状态。 
PhotoManager:该类管理所欲照片实例。

用dispatch_sync处理后台任务

回过头来看该app,从相册库添加一些图片或使用网络下载一些。 
关注下在点击UICollectionViewCell后,花费了多长时间去实例化显示一个新的PhotoDetailViewController,有明显的滞后,尤其是在反应慢的设备上预览大图时。 
在很多复杂的环境下,执行UIViewController’s viewDidLoad很容易过载,在新视图显示之前常常要等待较长时间,在加载时不是必要的工作可以放在后台处理。 
这听起来像是异步工作。 
打开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
        });
    });
}

上面是将要修改的代码。 
1、首先从把任务从主线程放到全局队列中。因为这是dispatch_async(异步),代码块被异步提交意味着将在从线程中调用。这可以让viewDidLoad可以尽快在主线程中执行完,让加载感觉特别快。同时,图片加载开始执行,将在之后某个时间完成。 
2、这时候,图片加载处理已完成,你已经生成一个新的图片。你可以拿新图片去更新显示。到主队列中添加一个新的工作。记住,只能在主线程中更新UI。 
3、最后,在fadeInNewImage中更新UI。 
生成并运行app,选择图片你会发现试图控制加载明显更快,并在短暂的时间后显示大图。这提供了一个不错的查看大图的效果。 
同样的,如果你试着加载一个出奇巨大的图片时,这个app也不会再加载视图控制器的时候卡住,同时app可以很好的扩展。 
正如上文提到的,dispatch_async将添加block到一个队列并立即返回。该任务将在一段时间之后被GCD决定执行。当需要执行一个网络操作或cpu耗时的任务时放在后台不会阻塞当前线程的执行。 
下面是一个如何、何时使用dispatch_async的各种队列类型的快速向导: 
自定义串行队列:当要后台串行执行任务、要跟踪它时,这是一个不错的选择。这消除了资源争用,因为你已经知道同一时间只能有一个任务执行。注意,如果你需要从一个方法获取数据,必须内嵌另一个 block进去同时考虑采用dispatch_sync方式。 
主队列(串行):在一个并行的队列中完成任务后去更新UI,选择它。这样做的话,将内嵌另一个block到block中。同理,如果在主队列中调用dispatch_async,只能保证新任务在当前方法结束后一段时间内将会执行。 
并行队列:要在后台执行非UI工作可以选择它。

延时工作dispatch_after 
考虑一下app的用户体验,当用户第一次打开app时可能会很困惑,不知道要做什么。 
展示一个提示信息可能是一个不错的主意,当在PhotoManager中没有任何照片时。但是,你也需要考虑用户的眼睛是如何浏览屏幕主页的,如果你展示图示信息太快(一闪而过)的话,他们可能根本没有看清视图中显示的内容。在显示提示信息时加上1~2秒的延时足够吸引用户注意了。 
添加下面的代码再执行去试着实现显示延时:(showOrHideNavPromote PhotoCollectionViewController)

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

生成并运行app。轻微的延迟,将吸引用户的注意,提示他们该怎么做。

dispathc_after工作就像一个延时的dispatch_async。你依然没有实际执行时间的控制权,但是可以在其返回之前取消。 
想知道什么时候使用dispatch_after吗? 
1、自定义串行的队列:在自定义串行队列上小心使用,最好在注队列使用。 
2、主队列:主队列可以使用dispatch_after,Xcode有一个nice模板去自动创建使用它。 
3、并发队列:在自定义的并发队列上使用dispatch_after时要小心,而且很少用。坚持在主队列使用它。

使单例模式线程安全

单例模式:既爱又恨,在iOS和在服务器器系统上的web一样受欢迎。 
复杂的单例关系常常不是线程安全的。这种关系要合理的使用:单例模式就是常常多个视图控制器同时访问单个单一实例。 
对单例来说,线程关系涉及到初始化、读取和写入信息。 
PhotoManager类就是单例类,在当前状态下就面临这些问题。为了更快的看到问题所在,将要在单例中创建一个受控的争用条件。 
定位到PhotoManager.m 然后找到sharedManger,代码想下面这样:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}
 当前状态下代码是很简单的,你创建了一个单例然后初始化了一个私有数组(photoArray)。
但是,if条件分支不是线程安全的,如果你多次调用它,很有可能出现在线程A中进入if代码段然后在执行sharedManager分配之前进行了上下文切换。然后在线程B中可能也进入if条件,分配了一个单实例而后退出。当系统上下文切换回线程A后,继续分配领一个单实例后退出。同时将产生两个单实例,这不是我们想看到的。
 为了防止这种情况发生,替换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;
}
 上面代码中,在线程休眠方法中强制进行上下文切换。打开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];
});
 这将创建多个异步并发调用来实例化单例并会出现上文所述的争用情况。
 生成并运行项目,检查控制台的输出,将会看到多个单实例初始化,如下所示:

这里写图片描述
注意:有几行显示了单例实例不同的地址,偏离了单例的初衷,不是吗? 
输出显示只应被执行一次的临界区却被执行了多次。诚然,现在是你强制这种情况发生,但是你可以想象一下这种情况也会在不经意间偶然出现。 
注:基于系统之上的其他事件很难控制,一系列的NSLog打印证明这点。线程问题很难跟踪因为它很难复现。 
为了纠正这种问题,当运行在临界区的if条件中时,初始化代码应该仅被执行一次并阻塞其他实例。这个正是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;
}

生成并运行app,查阅控制台输出,你讲看到仅有一个单实例被初始化,这才是我们想要的单例模式。
现在既然理解了防止争用条件的重要性,就删除AppDelegate.m中添加的diapatch_async语句然后替换单里初始化的实现用下面的实现:

+ (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中的基础容器是线程安全的吗?答案是-不是的。苹果维护了一系列有益的非线程安全的基础数据类型。 
尽管,很多线程可以读取一个可变的数组而没有问题,但让一个线程去修改数组在其他线程读取的时候是不安全的。你的单例模式没有避免这种情况发生。 
为了看到问题,看下addPhoto在PhotoManager.m中(已经被替换为如下:)

- (void)addPhoto:(Photo *)photo
{
    if (photo) {
        [_photosArray addObject:photo];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self postContentAddedNotification];
        });
    }
}
 这是一个写操作去修改一个可变的数组。
 现在修改photos属性如下:
- (NSArray *)photos
{
  return [NSArray arrayWithArray:_photosArray];
}
 这个属性的getter方法是一个读方法去访问这个可变数组。这个调用获得一份不可变的拷贝以免被不当破坏,但是没有提供任何保护(当一个线程正在写方法addPhoto时,另外线程去读这个属性)。
 这就是软件系统的读写问题。GCD提供了一个优雅的解决方案通过使用调度障碍来创建读写锁。
 调度障碍(栅栏)是一组函数像在并行队列中的串行式障碍一样。使用GCD阻塞API确保提交的闭包是该特定队列上在特定时间是唯一的被执行元素。这就意味着所有被提交到队列的元素必须在闭包被执行之前完成。
 当轮到该闭包时,阻塞执行闭包确保在这段时间内队列不会执行其他闭包。一旦完成,队列返回到默认实现位置。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、这是对象添加到数组的实际代码。因为这是一个障碍闭包,这个闭包将永远不会和其他闭包同时执行在该队列中。
      4、最后你发送了一个通知表明添加了一个图片。这个通知将从主线程发送因为它要处理UI工作。所以这里调度了一个异步任务到主队列去处理通知。
 注意写操作,你也需要实现图片读操作。
 为了确保写入方的线程安全,你需要在该队列中执行读操作。你需要从函数中返回,因此你不能异步调度执行因为那样将导致在读函数返回之前永远不会执行。
 在这种情况下,同步将是一个很好的选择。
 dispatch_sync同步提交任务然后等待直到完成才执行返回。使用dispatch_sync来保持跟踪你的dispatch_barrier工作,或在你可以通过闭包使用数据之前你需要等待操作完成。
 你需要小心。想想一下,如果你调用dispatch_sync然而作用目标即当前队列已经在执行了。这将导致死锁因为调用将等待闭包完成,但是闭包(它甚至不能开始)将在当前正在执行的、不可能结束的闭包结束后才能结束。这将迫使你意识到你正在调用的队列就是你正在传递的队列。
 下面是一个快速的预览何时何地可以使用dispatch_sync:
      1>、自定义串行队列:这种情况下要非常小心,如果你正在运行的一个队列正好是你dispatch_sync的目标队列这将导致死锁。
     2>、 主队列:要小心使用,原因跟上面一样。这种情况也同样存在潜在的死锁。
      3>、并行队列:这是一个不错的选择通过dispatch_barrier或者当等待一个任务完成以便你可以进行下一步操作时。

还在PhotoManager.m,用下面的实现替换属性:

- (NSArray *)photos
{
    __block NSArray *array; // 1
    dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
        array = [NSArray arrayWithArray:_photosArray]; // 3
    });
    return array;
}
 这是一个读函数,依次看注释就会发现:
     1、__block关键字允许在块内部改变该对象,没有这个的话,array在块内将是只读的,你的代码甚至不能通过编译。
      2、队列中的同步调度执行读操作
      3、保存photoArray的拷贝然后返回。

祝贺你,你的PhotoManager单例现在是线程安全的。无论在哪儿或者以何种方式读或写图片,它都将以线程安全的方式毫无意外的正常工作。 
最后,你需要初始化你的并发队列属性。像下面这样改变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创建一个并发的队列。第一个参数是反向DNS域名。这样的描述在调试的时候很有用。第二个参数标识队列是串行还是并行。
 注:当在web上查找例子时,你常常看到别人穿0或者NULL作为dispatch_queue_Create的第二个参数。这是一种已经过时的方式,使用具体的(系统提供的枚举类型DISPATCH_QUEUE_CONCURRENT等)作为参数总归是更好的。
 祝贺你,你的PhotoManager单例现在是线程安全的。无论在哪儿或者以何种方式读或写图片,它都将以线程安全的方式毫无意外的正常工作。

视图形式查看排队

 现在是不是还是不能完全掌握GCD的要点?确信可以使用GCD方法创建简单的例子并且使用断点和NSLog去了解正在发生的事情(GCD执行过程中)。
 下面提供了两个GIFs的例子帮助你加强dispatch_async和dispatch_sync的理解。代码以辅助可视化工具的形式包含在GIF图中,注意左边GIF中断点的每一步以及右边关联队列的状态。

dispatch_sync初探:

override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_sync(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}

这里写图片描述
下面简要介绍下关系图表: 
1、主队列按序执行,接下来就是一个任务初始化(视图控制器初始化加载viewDidLoad)。 
2、viewDidLoad在主线程执行。 
3、同步闭包被添加到全局队列且稍后执行。进程上表现是主线程停止知道该闭包完成。同时,全局队列是并发执行任务的,在全局队列上是按FIFO的顺序唤起闭包的执行,但是可能是并发执行的。全局队列同时还处理在该同步闭包添加到队列之前就已经存在的任务。 
4、最后,该同步闭包按序执行。 
5、该闭包完成之后,主线程才被唤醒。 
6、viewDidLoad方法执行完毕,主队列接着处理其他任务。 
同步添加任务到队列,然后等待直到该任务完成。异步添加的话,唯一不同的是在被调起的线程里无需等待任务完成就可以继续执行向下执行。

dispatch_async初探:
override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_async(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}

这里写图片描述
1、主队列按序执行,接下来就是初始化一个视图控制器任务,viewDidLoad在主线程执行。 
2、viewDidLoad在主线程执行。 
3、主线程现在在viewDidLoad里,刚好执行到dispatch_async。 
4、异步闭包被添加到全局队列,稍后执行。 
5、viewDidLoad在异步闭包添加到全局队列之后继续执行,主线程继续执行未完成的任务。同时,全局队列并发的执行其未完成的任务。记住:全局队列任务是按FIFO顺序出栈执行,但是执行过程中可以是并发的。 
6、被添加的异步闭包正在执行。 
7、异步闭包完成,同时控制台已经有NSLog输出。 
在该热定情况下,第二个NSLog紧随第一个NSLog执行。但并非总是如此,这取决于正在执行的硬件时间,你没有办法知道那个输出先执行。在一些调用中先执行的NSLog可能是第一个NSLog中执行的。

接下来学习什么?

在这个教程中,已经了解了如何使代码线程安全,以及在CPU多任务处理下如何保持主线程的响应能力。 
你可以下载GooglyPuff Project 工程,其中包含了所有目前教程中的实现。第二部分的教程中你将去改进这个项目。 
如果你计划优化你的app,你应该使用Instruments进行性能分析。使用这个工具超出了本教程的范围,所以你应该查阅一些关于如何使用它的优秀文章。 
确保有真机可以使用,因为模拟器测试出来的记过和真实用户使用的体验反馈是不一样的。 
在下一部分(https://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2)的教程中,你将深入的连接GCD的API来做一些更酷的东西。 
如果有问题或者建议,可以自由的加入下面的讨论区。

 ***<第一次做翻译工作,太难了。做的不好,欢迎各位大大们不吝批评指正。万分感谢!>***
***<另外格式也不太好,弄完之后从印象笔记拷出来的,博客的格式编辑有点难搞,就这样将就着看吧。>***
作者:ldz15838245189 发表于2016/8/16 17:30:47 原文链接
阅读:29 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>