GCD in depth(GCD深入理解)

iOS

本文原文为raywenderlich“GCD深入理解”。一贯的风格,示例图解GCD的日常用法;part 1介绍GCD/多线程的基本背景,part 2介绍GCD常用API。(本文节选)

GCD是什么

GCD是指libdispatch,Apple公司的一个代码库,用来支持多核设备(iOS/OS X)上的代码并发执行。使用有如下好处:

  • 通过后台执行计算密集任务、提高app响应速度
  • 提供比锁/线程方便的并发模型、减少并发bug
  • 代码优化、以原语(primitives)方式为常见模式提供更高的性能(如单例模式)

GCD术语

串行 vs. 并发(Serial vs. Concurrent)

描述的是当前任务与其他任务之间的关系,或者说是对任务管理队列执行形式的描述。串行执行,同一时间只有一个任务被执行;并发执行则同一时间可以有多个任务被执行。这里的「同一时间」我们指的是面向用户的时间,因为对于单核计算机,在指令执行层面都是串行的。另一个相关的概念“并行”将在下文中单独探讨。

同步 vs. 异步(Synchronous vs. Asynchronous)

描述的是提交任务到dispatch的函数何时返回的问题。一个“同步”的函数,必须等待它提交的任务执行完之后才返回;而“异步”的函数则是将任务提交给dispatch后立即返回。因此“同步”的函数是会阻塞当前线程的,而“异步”的函数则不然。当然“异步”的函数也不管任务的具体执行问题,这是dispatch内部的事情。

临界区(Critical Section)

临界资源是指需要互斥访问的共享资源,临界区则是指访问共享资源的一段代码,临界区处理不当会导致变量不合预期的被修改。

竞争条件(Race Condition)

多个线程或进程在读写一个共享数据时结果依赖于他们执行的相对时间。因此不同的时机会影响系统以不受控的方式运行。

死锁(Deadlock)

两个或多个任务(大多数情况下是线程),如果都陷入等待对方的执行完成来结束自己的执行,会导致死锁。

线程安全(Thread Safe)

线程安全的代码能被多个线程或并发任务调用而不会产生任何问题(数据污染、闪退等),非线程的代码一次只能被运行在一个环境(context)下;线程安全的例如非可变集合类型NSDictionary。

上下文切换(Context Switch)

上下文切换指的是在一个进程中切换执行不同线程时进行的状态存储或恢复的操作;

并发 vs 并行(Concurrency vs. Parallelism)

并发及并行总被一起提及,因此这里需要解释以区分它们。
并行指的是指令级别的同时执行,是需要设备多核心的支持;并发代码的”同时“执行,指的是任务级别的“同时”执行,是在任务的时间尺度里,可以在多核设备上通过并行来支持,也可以通过上下文切换(Context-Switch)来支持。多核设备上通过并行(parallelism)来同时执行多个线程,而在单核设备上则需要进行上下文切换;关系如下图示:

concurrency-parallelism

Parallelism requires concurrency, but concurrency does not guaranteeparallelism

并发实际上描述的是结构(structure),这是实质的不同。作者荐读

队列(Queues)

GCD提供了dispatch queue来处理代码块;这些队列管理着你提交给GCD的任务(tasks)并以FIFO的方式来执行他们。所有dispatch queues本身是线程安全的,因此可以在多线程中同时访问他们。关键在于选取合适的分发队列(dispatch queue)以及正确的dispatch方法来提交你的任务到队列。

GCD中两种不同的dispatch队列,串行及并发执行队列。

串行队列(Serial Queue)

串行队列中的任务每次只执行一个,每个任务只能等待前面的任务完成后才会开始执行。当然你并指导一个代码块结束与下一代码块开始之间的耗时,如下图所示:
serial queue

这些任务的执行时间由GCD来控制,你唯一得到的保证就是,GCD每次仅执行一个任务并且任务执行顺序跟他们添加的顺序是一致的。
串行队列中任务不可能并发执行,因此不会有临界区访问竞争条件等的问题/风险。因此也可以使用串行队列来规避一些多线程问题。

并发队列(Concurrent Queues)

并发队列中的任务唯一能保证开始执行时机与被添加的顺序一致。任务能以任意的顺序结束,而你也不知道将耗费多长时间来启动下一个任务或者特定时间点有多少任务在执行。这些都是由GCD来控制。并行队列中的任务执行如下图所示:
concurrent queue

GCD提供了至少5种特定的队列类型。

队列类型(Queue Type)

1、主队列(main queue):首先系统提供了一个特殊的串行队列也就是熟知的主队列。跟其他串行队列一样,主队列中的任务同一时间也只能执行一个。但是它保证了所有任务都是在主线程中执行的,这是唯一允许更新UI的线程。这个队列用来给UIView发消息或者发送通知。

2、全局队列(Global Dispatch Queues):系统提供了几个并发队列,也就是所谓的全局派发队列。目前有四个不同优先级的全局队列:background,low,default以及high。需要注意的是,苹果的API也会调用这些队列!

3、自定义队列:系统允许我们创建自己的串行或并发队列。

GCD的“艺术”在于选择正确的队列和正确的分发方法(dispatch function)来提交你的任务。可以跟下面的例子走一遍,来理解这一点。

实例

使用dispatch_sync/dispatch_async处理后台任务

示例代码:

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidLoad
{
[super viewDidLoad];
self.photoImageView.image = _image;
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将一个代码块提交到队列中,然后立即返回;代码块任务将在之后由GCD决定决定何时执行。下面是几个使用dispatch_async的几个不同队列的介绍:

  • 自定义串行队列:任务在后台同步执行,消除了资源竞争。注意若需要从一个方法获取数据,你必须在block中内联一个block来获取,或者使用dispatch_sync。
  • 主线程队列(串行):这是在并发队列中完成任务后更新UI的常用方法。
  • 同步队列:在后台执行非UI工作的常用方法。

dispatch_sync与dispatch_async是串行/异步的区别(见上文);dispatch_sync会向队列提交任务,然后等任务完成后才返回(执行下一个指令)。使用dispatch_sync及__block可以捕获任务执行中的变量。

注意!假如你调用dispatch_sync并且将目标队列设置为你当前运行的队列,这将会导致死锁。因为dispatch_sync的调用需要等待任务block执行完成来返回,但是任务block不会结束(其实甚至还没开始)直到队列当前执行的任务结束,而这是办不到的!因此调用dispatch_sync时你必须清楚自己所处的队列以及目标队列。
快速预览dispatch_sync的使用场景:

  • 自定义串行队列:务必小心,如果你调用dispatch_sync时指定的目标队列跟调用时所处是同一个队列,这必然会形成死锁!
  • 主线程(串行):务必小心,理由同上
  • 并发队列:就这样可以的!可以使用dispatch_barrier_(a)sync或者dispatch_sync

栗子:

1
2
3
4
5
6
7
8
- (NSArray *)photos
{
__block NSArray *array; // 1
dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
array = [NSArray arrayWithArray:_photosArray]; // 3
});
return array;
}

使用dispatch_after延时处理

示例代码:

1
2
3
4
5
6
7
8
9
double delayInSeconds = 1.0;
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){ // 2
if (!count) {
[self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"];
} else {
[self.navigationItem setPrompt:nil];
}
});

使用dispatch_once构建单例

1
2
3
4
5
6
7
8
9
10
+ (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-barrier处理“读者-写者问题”

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)addPhoto:(Photo *)photo
{
if (photo) {
[_photosArray addObject:photo];
dispatch_async(dispatch_get_main_queue(), ^{
[self postContentAddedNotification];
});
}
}
- (NSArray *)photos
{
return [NSArray arrayWithArray:_photosArray];
}

上述的NSMutableArray类型的属性不是线程读写安全的,即使dispatch_once保证了单例的,photos属性也对外暴露inmutable的接口,但是并不能避免多线程读写的问题。

这个经典读写者同步的问题可参考这里
GCD提供了一个优雅的解决方案,通过dispatch barriers创建读写锁。懒,直接看原文

Dispatch barriers are a group of functions acting as a serial-style bottleneck when working with concurrent queues. Using GCD’s barrier API ensures that the submitted block is the only item executed on the specified queue for that particular time. This means that all items submitted to the queue prior to the dispatch barrier must complete before the block will execute.
When the block’s turn arrives, the barrier executes the block and ensures that the queue does not execute any other blocks during that time. Once finished, the queue returns to its default implementation. GCD provides both synchronous and asynchronous barrier functions.

dispatch-barrier图示如下:

dispatch-barrier提交的代码块执行时的队列就如串行队列(队列中只有barrier的代码执行),但除此外队列恢复原本的方式(通常是并发队列)。下面是你是否应该使用barrier方法的一些说明:

  • 自定义串行队列:这是个糟糕的选择,串行队列本身就是每次只有一个任务在执行,dispatch-barrier在这里帮不上什么忙
  • 全局并发队列:小心!因为之前提过的,其他系统可能也在使用这些队列!(据为己有是危险的。。
  • 自定义并发队列:对于原子操作或临界区代码执行,这是个很好的选择。设置或者实例化需要线程安全的地方,这是个很好的选择。

//创建信号量,参数:信号量的初值,如果小于0则会返回NULL

dispatch_semaphore_create(信号量值)

//等待降低信号量

dispatch_semaphore_wait(信号量,等待时间)

//提高信号量

dispatch_semaphore_signal(信号量)

Author: Jason

Permalink: http://blog.knpc21.com/ios/translated-gcd-in-depth-i/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。

Comments