0%

【WWDC】Mastering Grand Central Dispatch

前言

本文是对Mastering Grand Central Dispatch WWDC2011 - Session 210以及 GCD 其他内容的学习笔记。

GCD 按照我的理解就是一个系统级的并发模型,通过这个模型你不再需要直接接触底层的“线程”和“锁”去编写大量的对线程安全要求很高的代码,而是在模型中将“执行任务的 Block”添加进合适的“调度队列(dispatch queue)”中就可以轻松实现多线程编程。当然它还附赠了例如dispatch_once在线程安全的环境下实现单例等很多好用的功能!

要编写多线程代码,首先要理解下面这些基本的概念:

基本概念

进程(Process)与线程(Thread)

进程是程序(指令与数据)的真正运行实例。在早期面向进程设计的操作系统中,进程是程序的基本执行实体;而现代面向线程设计的操作系统中,进程本身不是基本运行单位而是线程的容器。计算机可以同时运行多个进程,有前台进程也有后台进程。线程是进程中的实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

并行(Parallelism)与并发(Concurrency)

Grand Central Dispatch Tutorial for Swift 3: Part 1/2中的这张图很清晰的解释了两者的区别:

Concurrency_vs_Parallelism
在单核 CPU 中通过“上下文切换”(即保存 CPU 的寄存器等信息到各自路径专用的内存块中,从切换目标路径专用的内存块中复原寄存器等信息,继续执行目标路径的命令列的过程)在某个线程与其他线程间反复执行,并发的看上去像是同时执行多个线程一样。而在多核 CPU 中则可以通过并行真正的同时执行多个线程。

GCD 是建立在线程的基础之上的,在下层系统维护着一个共享的线程池,你只需要将执行任务的 block 或者函数添加进 dispatch queues 中,然后由 GCD 来决定在哪个线程上面执行它。GCD 根据系统状态及可用资源来决定进行多大程度的并行。留意并行要求必须是并发的,但是并发并不能保证一定并行!

分发队列(Dispatch Queue)

Dispatch Queue 是管理你添加的要执行任务的队列,它按照先进先出(FIFO)的顺序执行处理。Dispatch Queue 是线程安全的,也就意味着你可以同时在多个线程访问它。Dispatch Queue 分为 Serial Queue和 Concurrent Queue 两种(注:下面部分图片来自 Ray 家的教程):

串行队列(Serial Queue)

Serial-Queue-Swift
Serial Queue 保证在任何时间同时只能执行一个任务,它会等待当前正在进行的任务结束之后再处理其他任务。由 GCD 来控制任务的执行时机,两个任务之间会有多久的间隔也是不确定的。一旦生成 Serial Queue 并添加了任务进去,系统对于一个 Serial Queue 就只生成并使用一个线程。但是多个 Serial Queue 对应各自不同的线程,因此他们之间是可以并行执行的。Serial Queue 与线程的关系可以看下面的gif图(注:图片截取自 WWDC 视频):

2018-07-09 23_21_23

并发队列(Concurrent Queue)

Concurrent-Queue-Swift

Concurrent Queue 允许同时执行多个任务,其中的任务会按照被添加的顺序出队开始执行,但是它不会等待正在执行的任务结束就可以开始下一个任务。任务结束的顺序及同一时刻正在执行的任务数量是不确定的。一个 Concurrent Queue 可以使用多个线程同时执行多个处理。什么时候开始执行一个任务,在多核环境下是使用“上下文切换”还是在另一个核心上运行这些都是由 GCD 决定的。Concurrent Queue 与线程的对应关系可以看下图:

2018-07-10 00_02_32

常用 API

dispatch_get_main_queue/dispatch_get_global_queue

有两种途径得到Dispatch Queue:

  1. 获取系统标准提供的 Main Queue/Global Queues
  2. 通过dispatch_queue_create函数生成的 Custom Queues

Main Queue 中的任务是在主线程 Runloop 中执行的,因为主线程只有一个,Main Queue 自然也就是一个 Serial Queue。由于在主线程中执行,因此要将用户界面的更新等必须在主线程中执行的任务添加到 Main Queue 中。可以通过dispatch_get_main_queue()函数获取。

Global Queue 是整个系统共享的 Concurrent Queue,它包含四个不同优先级的队列:high, default, low, 和 background。在 iOS 8.0 之前,只需要将任务提交到对应执行优先级的 Global Queue 中即可保证任务执行的优先级顺序。例如获取一个高优先级队列的函数如下:

1
2
//第二个参数 flag 是为未来保留的,现在传入 0 即可。
dispatch_queue_t highGlobalQue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

现在你不需要直接指定 Queue 的优先级了,而是指定一个 Quality of Service (QoS) 级别来暗示任务的重要性,然后 GCD 通过这个值来决定使用哪个优先级的队列。Qos 级别有以下几种:

  • QOS_CLASS_USER_INTERACTIVE: 这个级别代表任务需要尽快执行以便提供良好的用户体验,主要用于 UI 刷新,处理事件等需要低延迟的任务。这个级别的任务应该在主线程执行。
  • QOS_CLASS_USER_INITIATED: 这个级别代表任务是从 UI 线程初始化的,可以异步执行,主要用于尽快的得到执行结果,这个级别被映射到了高优先级队列。
  • QOS_CLASS_DEFAULT: 默认的任务级别。映射到默认优先级队列。
  • QOS_CLASS_UTILITY: 这个级别的任务通常是耗时的,主要用于计算,磁盘IO,网路通信等,它被设置为尽量节省能源的。这个级别被映射到了低优先级队列。
  • QOS_CLASS_BACKGROUND:这个级别的任务大多是用户感知不到的,会被映射到后台优先级队列
  • QOS_CLASS_UNSPECIFIED:这个级别表示服务质量缺失。

获取一个QOS_CLASS_USER_INITIATED级别的队列方法如下:

1
dispatch_queue_t userInitiatedQue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);

dispatch_queue_create

1
2
3
4
//第一个参数"com.zane.serialQueue"用来标记这个队列,在 Instruments 中调试时作为队列的名字。第二个参数使用 NULL/DISPATCH_QUEUE_SERIAL 表示生成串行队列,使用 DISPATCH_QUEUE_CONCURRENT 表示生成并发队列
dispatch_queue_t customeSerialQue =dispatch_queue_create("com.zane.serialQueue", NULL);

dispatch_queue_t customeConcurrentQue = dispatch_queue_create("com.zane.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

上面代码创建的 Serial Queue,在往这个队列中提交了任务之后,系统就会为它生成一个线程。使用这个方法创建多少个 Serial Queue,就会对应生成多少个相互间并行的线程,因此如果创建大量的 Serial Queue 就会消耗大量内存,引起大量上下文切换,使得程序性能降低因此只应该在多个线程更新共享资源会导致数据竞争时,使用一个 Serial Queue.

上面创建的 Concurrent Queue,不管创建多少个,系统都只会为其使用有效管理的几个线程。

早期通过这个方法生成的 Queue 还需要使用dispatch_release()函数释放它,iOS 6 之后,系统通过ARC 来管理生成的 Queue,提交进 Queue 的 block 会持有一个该 Queue 的引用,所以只有在 Queue 中所有的 block 执行完之后才会释放。

dispatch_sync/dispatch_async

可以通过下面两个常用的函数向 Queue 中添加任务:

1
2
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

dispatch_async函数是非同步(asynchronous)的,他将 block 添加进 queue 中,不做任何等待立即返回,这样不会阻碍当前执行的线程,可以继续进行下一行函数,常用于处理后台任务。dispatch_async函数内部会先将 block 拷贝到堆中,避免 block 执行前就被销毁。

dispatch_sync函数是同步(synchronous)的,它将 block “同步的”添加进 queue 中,在添加进的这个 block 任务执行完成之前,dispatch_sync函数会一直等待,该函数会处于调用状态而不返回。也就是说当前线程是停止的。当 block 中的任务执行结束之后,函数返回,当前线程继续进行下一行函数。

dispatch_async不同,dispatch_sync不会对该 queue 执行 retain 操作,它从调用者那里“借来”一个对 queue 的引用,而且也不会对 block 做Block_copy操作。

dispatch_sync函数的特性导致其很容易引起死锁的情况,如在执行一个 Serial Queue 的任务的线程中,将一个任务 block 同步添加到这个 Serial Queue 中时,就会发生死锁。因为该线程中正在执行这个函数,它已经处于调用状态而不返回,也就无法执行 block 中的任务,两者互相等待,形成死锁。实例代码如下:

1
2
3
4
5
6
dispatch_queue_t customeSerialQue =dispatch_queue_create("com.zane.serialQueue", NULL);
dispatch_async(customeSerialQue, ^{
dispatch_sync(customeSerialQue, ^{
NSLog(@"work");
});
});

其实还有相对应的xxx_f的直接提交函数任务的 API,上面的两个函数实现中其实也是封装调用了下面的函数。

1
2
3
//当提交的 work 函数调用时,context 作为第一个参数传递给 work 函数,表示上下文数据。work 函数不能为空。
void dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
void dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);

dispatch_barrier_sync/dispatch_barrier_async

这个函数通常用于处理“读者写者”的问题。当多个线程同时对一份数据进行操作时,这时就很容易出现“线程不安全”的问题,在 GCD 出现之前我们通常使用@synchronized()锁或者NSLock锁来提供同步机制。如下所示:

1
2
3
4
5
6
7
8
9
10
- (NSString *)someString{
@synchronized(self){
return _someString;
}
}
-(void)setSomeString:(NSString *)someString {
@synchronized(self){
_someString = someString;
}
}

这种写法会给对象self自动创建一个锁,等块中的代码执行完毕后就释放这个锁。它的缺点在于如果你代码中有大量的@synchronized(self)时,他们都共用同一个锁,程序可能会等待另一段于此无关的代码执行完毕。

有 GCD 之后,我们可以使用 Serial Queue 来提供替代方案,将读取与写入操作都写入同一个 Serial Queue 中,即可保证数据同步。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_customeSerialQue =dispatch_queue_create("com.zane.serialQueue", NULL);
//...
- (NSString *)someString{
__block NSString *tempString;
dispatch_sync(_customeSerialQue, ^{
tempString = _someString;
});
return tempString;
}

-(void)setSomeString:(NSString *)someString {
//注:其实设置方法也可以使用 dispatch_async,因为这里并不需要等待返回值。
dispatch_sync(_customeSerialQue, ^{
_someString = someString;
});
}

更进一步,我们可以发现这个问题中,多个读取方法是可以并发执行的,但是读取方法与写入方法之间不能并行执行。这时就可以请出dispatch_barrier_sync/dispatch_barrier_async方法了。他们在 Concurrent Queue 上工作时提供了一个串行式的瓶颈,在队列中,通过他们提交的任务必须单独执行,这就意味着在 barrier 之前提交的任务必须先全部完成,然后再单独执行 barrier 提交的任务,执行完成后,队列又恢复正常的并发状态。这些特性意味着该方法只对 Concurrent Queue 有意义,因为 Serial Queue 本身就是一次执行一个任务的。

Dispatch-Barrier-Swift
再看一个动图,加深理解:
2018-07-12 10_40_14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_customeConcurrentQue =dispatch_queue_create("com.zane.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//...
- (NSString *)someString{
__block NSString *tempString;
dispatch_sync(_customeConcurrentQue, ^{
tempString = _someString;
});
return tempString;
}
-(void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_customeConcurrentQue, ^{
_someString = someString;
});
}

“读者写者问题”的实例代码如上,还有一点需要留意的是在上述代码中我们使用了dispatch_queue_create生成的自定义的并发队列,而没有使用dispatch_get_global_queue获取系统的全局并发队列,这是因为全局并发队列是系统资源,他不喜欢你来操纵它,所以dispatch_barrier_async函数用在它上面是没有效果的,效果跟使用dispatch_async函数一样。同理,可以暂时挂起队列和恢复队列执行的dispatch_suspend/dispatch_resume对于全局并发队列也是没有效果的。

dispatch_after

dispatch_after用于一个任务的延后执行。它并不是在指定的时间执行,而是在指定的时间异步的将任务添加进 queue 中。它的一个简单的示例如下:

1
2
3
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"work");
});

这段代码与 3 秒后使用dispatch_async函数将 block 添加进 Main Queue 中的效果是一样的。它的第一个参数是dispatch_time_t类型的值,一般通过dispatch_time函数或者dispatch_walltime函数获取,前者通常用于计算相对时间,后者用于计算绝对时间。这个参数传入DISPATCH_TIME_NOW 的话,倒不如直接使用dispatch_async函数。传入DISPATCH_TIME_FOREVER结果是未定义的。

dispatch_once

dispatch_once是用来保证在应用程序中只执行一次处理的 API。它是线程安全的操作,因此几乎成了 Objctive-C 中实现单例的标准方法了:

1
2
3
4
5
6
7
8
+ (instancetype)sharedInstance {
static SomeClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

dispatch_once的第一个参数是一个指向dispatch_once_t的指针,用来测试 block 中的任务是否已经完成。对于只需执行一次的任务来说,每次调用传入的该标记值应该完全相同。所以该指针指向的变量应该为静态变量或者全局变量。

调度组(Dispatch Group)

Dispatch Group 涉及到集合的同步化,你将多个任务添加到一个分组之后,就可以等待所有的任务执行完毕,或者提供一个回调函数后继续往下执行,当所有任务执行完毕后收到通知,执行回调函数。更厉害的是你提交的任务可以属于不同的 Queue。这种特性常用于必须在指定的任务都完成的情况下才能继续的情况。

先看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
//1生成一个 group
dispatch_group_t group = dispatch_group_create();
//2异步添加任务
dispatch_group_async(group, queue, ^{
NSLog(@"work1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"work2");
});
dispatch_group_async(group, dispatch_get_main_queue(), ^{
NSLog(@"work3");
});
//3挂起线程,等待任务完成或超时
long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (int64_t)2*NSEC_PER_SEC));
if(result == 0) {
NSLog(@"work done");
}else {
NSLog(@"working");
}

我们先通过dispatch_group_create方法生成一个 group, 然后通过dispatch_group_async函数将 block 添加进 queue 中并与 group 联系起来之后,group 就会保持一个其中未完成任务的数量值,连接一个任务时增加该值,任务完成后减少该值。后面的dispatch_group_waitdispatch_group_notify函数就是使用这个数量值来判断与这个 group 连接起来的所有任务是否完成。

dispatch_group_async函数与dispatch_async相似,只是多了将任务 block 与 group 联系起来的作用。

在这里我们使用了dispatch_group_wait函数,它会一直处于调用状态而不返回,从而阻塞了当前线程,直到 group 中的所有任务执行完成或者到达指定的时间。返回时若所有任务都执行完成,这个函数的返回值为 0,否则不为 0。你也可以指定时间参数为DISPATCH_TIME_FOREVER让它一直等待直到全部任务完成。

除了上面这种同步等待的方式,还可以使用dispatch_group_notify来异步的获取通知,它的调用不会阻塞当前线程,直接返回。在检测到队列中的所有任务完成时,dispatch_group_notify会将执行结束处理的 block 添加到 它指定的 queue 中。上例中的注释 3 后的部分可以替换成下面这样:

1
2
3
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"work done!");
});

还有一个特殊的需求,试想:如果我们打算在任务 block 中添加一个想要异步执行的任务,比如下载一张图片等等,这个时候若使用下面这种方式添加任务:

1
2
3
4
5
6
7
8
dispatch_group_async(group, queue, ^{
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:@"/images/thehitchhiker2.png"];
NSURLSessionDownloadTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"download done!");
}];
[task resume];
});

你可能不会达到期望的效果,程序可能会在打印了work done!之后再打印download done!,这是因为连接在 group 上的任务是异步的,它的 block 程序已经执行完了,但是它的实际下载并没有完成,而如果我们想观察的是下载任务的结束的话就需要请出dispatch_group_enterdispatch_group_leave的组合了。实例如下:

1
2
3
4
5
6
7
8
dispatch_group_enter(group);
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:@"/images/thehitchhiker2.png"];
NSURLSessionDownloadTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"work done");
dispatch_group_leave(group);
}];
[task resume];

这样就能达到我们的目的了。dispatch_group_enter函数表示一个任务进入这个 group 了,会增加队列中未完成任务的数量值,dispatch_group_leave表示一个任务已经完成,会减少队列中未完成任务的数量值。这两个函数的组合使得我们可以更合理的控制 group 中未完成任务的个数,从而达到更精确的控制。dispatch_group_enterdispatch_group_leave应该彼此对应,如果 enter 了而没有 leave,那么这一组任务将永远不会完成。

dispatch_apply

1
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

这个 API 会将带参数的 block 提交到 queue 中,用于多次调用。调用次数由第一个参数 iterations 决定,并且他会阻塞当前线程等待任务的迭代次数完成后才返回。若 queue 参数是 Concurrent queue 的话,那么这些迭代任务可以并发执行。若是 Serial queue 的话,且该 queue 对应的线程是当前正在运行的线程,则会与disparch_sync一样发生死锁的情况。注意若迭代次数过多或者执行任务很简单的话,该函数所带来的并发收益不能抵消创建线程等其他开销,这种情况下最好还是使用for循环。

dispatch_set_target_queue

1
void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

这个 API 可以为一个 object 设置 target queue,这个 object 可以是 dispatch queue、dispatch source或者 dispatch io 等等,target queue 最终决定了 object 所包含的任务会在哪个 queue 被调用。

queue 之间会形成一套层级体系,除了 Global queue 之外其他的 queue 都有其 target queue, queue 中的任务最终会在其 target queue 中执行。Global queue 在这个层级的最上层,沿着这个层级体系,所有的 queue 最终其 target queue 都会指向 Global queue和 Main queue。由 Global queue 的优先级(上文中讲到 Global queue 有如下优先级:high, default, low, 和 background)决定最终的执行优先级。使用dispatch_queue_create生成的 queue, 不管是 Serial queue 还是 Concurrent queue 其 target queue 默认都是 default 优先级的 Global queue。

dispatch_set_target_queue 的过程是类似dispatch_barrier_async,所以它不会影响已经添加在 queue 中的任务的执行过程,只会影响设置完 target queue 之后添加的任务。要留意设置 target queue 时不要形成循环。

修改一个 object 的 target queue 会影响它原本的行为:

  • Dispatch queues:将一个 queue 的 target queue 设置为 Serial queue 会同步化这个 queue。例如下图中的层级体系中(图片来自Effective Objective-C 2.0),排在 queue B与 queue C 中的 block 会在 queue A 中依次执行。于是排列在 queue A、B、C中的 block 总会错开串行执行(他们之间没有固定排序)。但是 queue A 与 queue D 中的 block 则可以并行执行。target queue hierarchy当然,如果你将一个 concurrent queue 的 target queue 指向 serial queue,那么其中的任务会串行的执行。
  • Dispatch sources: dispatch source 的 target queue 决定了它的事件处理的 block 将会被被提交到哪个 queue 中。
  • Dispatch I/O channels:如果一个 Dispatch I/O 的 target queue 被设置为 background 优先级的 Global queue 时,dispatch_io_readdispatch_io_write这些操作将会被节流。

若你有向一个 Serial queue 的前端添加任务的需求的话,使用dispatch_set_target_queue也可以满足你的需求:
jump the queue

dispatch_queue_set_specific/dispatch_get_specific

1
2
void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);
void * dispatch_get_specific(const void *key);

dispatch_queue_set_specific适用于将任意数据以键值对的形式关联到指定的 queue 中。留意与NSDictionary中的对象比较不同的是这个函数中的 key 比较的是指针值,而不是指针所指向的对象。所以可以使用一个静态变量的指针来传入这个参数。不建议直接传入一个字符串常量。context 参数是与 key 关联的上下文参数,可以为 NULL。在 destructor 参数中可以释放前面的 context 参数,当队列释放时,或者有新的值与该 key 关联时,原有的值就会被移除,这个析构函数就会被调用。

dispatch_get_specific这个函数需要在一个执行于 queue 中的 block 中调用,用以获取这个 queue 关联的数据,若不是在 queue 中运行的代码中调用则会返回 NULL。需要注意的是这个函数如果在指定的 queue 中查找不到 key 对应的数据,则会沿着 queue 的层级体系一直向上查找,直到找到数据或者达到根队列位置。要是传入的 queue 是 Global queue 也会返回 NULL。

dispatch_get_specific的特性可以用于解决一个 queue 层级间的同步化导致死锁的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dispatch_queue_t serialQueueA = dispatch_queue_create("com.zane.serialQueueA", NULL);
dispatch_queue_t serialQueueB = dispatch_queue_create("com.zane.serialQueueB", NULL);

dispatch_set_target_queue(serialQueueB, serialQueueA);

static int kQueueSpecific;
CFStringRef stringValue = CFSTR("queueA");
//析构函数只能带有一个指针参数且返回值为空,这里可以使用 CFRelease 作为析构函数
dispatch_queue_set_specific(serialQueueA, &kQueueSpecific, (void *)stringValue, (dispatch_function_t)CFRelease);

//通过 queue 特有关联数据,判断若在 queueA 中则直接执行 block,若不在则同步到 queueA 中执行,以此避免同步队列的死锁。
dispatch_sync(serialQueueB, ^{
dispatch_block_t block = ^{NSLog(@"work");};
//dispatch_get_specific 在 queueB 中找不到,再去它的 target queue 中寻找。
CFStringRef specificValue = dispatch_get_specific(&kQueueSpecific);
if (specificValue) {
block();
}else {
dispatch_sync(serialQueueA, block);
}
});

dispatch_queue_get_label()

在 iOS 中,如果我们要判断代码是否运行在主线程,可以直接使用 NSThread.isMainThread()方法。但如果要判断是否运行在主队列(main queue)呢?

需要注意的是,每个应用都只有一个主线程,但主线程中可能有多个队列,则不仅仅只有主队列,所以 NSThread.isMainThread() 方法并没有办法判断是否是在主队列运行。而GCD也没有提供相应的方法。那该如何处理呢?

在AFNetworking中有如下处理办法:

1
2
3
4
5
6
7
8
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {
block();
} else {
dispatch_async(dispatch_get_main_queue(), block);
}
#endif

信号量(Dispatch Semaphore)

信号量是一个同步对象,用于保持在 0 至指定最大值之间的一个计数值。当线程完成一次对该 semaphore 对象的等待(wait)时,该计数值减一;当线程完成一次对 semaphore 对象的释放(release)时,计数值加一。当计数值为0,则线程等待该 semaphore 对象不再能成功直至该 semaphore 对象变成 signaled 状态。semaphore 对象的计数值大于 0,为 signaled 状态;计数值等于0,为 nonsignaled 状态。

我们通过下面的例子看看 GCD 中的信号量怎么用:

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
//创建一个信号量,初始值为1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *mutableArray = [NSMutableArray array];
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
//当信号量大于等于1时,dispatch_semaphore_wait 会将信号量减 1 ,并返回。若信号量小于1 ,则会一直等待,直到信号量大于1或者超时。该函数的返回值与 dispatch_group_wait 函数一样。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[mutableArray addObject:@(i)];
//在执行完需要同步的任务后,将信号量的值加 1,使得其他线程中最先等待该信号量的代码继续执行。
dispatch_semaphore_signal(semaphore);
});
}

需要留意的是当信号量变量还在使用时,对该变量进行重新赋值或者置空会释放之前的信号量,从而引起崩溃!

Dispatch Source

Dispatch Source 是 BSD 系统内核惯有功能 kqueue 的包装,kqueue是在 XNU 内核中发生各种系统事件(例如 Unix 信号、文件描述符、Mach 端口事件、定时器等等)时,在应用程序编程方执行处理的技术。其内存占用小,尽量不占用资源,可以说是应用程序处理 XNU 内核中发生的各种时间的方法中最优秀的一种。

Dispatch Source 可以在这些事件发生时,在指定的 queue 中执行事件的处理。下面我们通过一个使用 Dispatch Source 设置定时器的例子看看他的简单用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//创建一个 dispatch source 来监控底层系统事件,当事件发生时,将相应的 handler block 提交到指定 queue 中。第一个参数为监控的时间类型,第二个参数是与第一个参数有关的信号编号,地灿哥参数也是与第一个参数有关的特定时间标志,第四个参数为提交 handler block 的 queue。这个创建过程是异步执行的。
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//设置定时器,第三个参数为间隔时间,这里设置为 DISPATCH_TIME_FOREVER 表示不需要重复。第四个参数为允许系统延迟的时间。注意即使该值为0,系统也可能会延迟这个定时器的执行。
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 1 * NSEC_PER_SEC);
//定时器指定时间所需要提交到 queue 中执行的任务 block
dispatch_source_set_event_handler(timer, ^{
NSLog(@"timer fired!");
//可以使用 dispatch_source_cancel 来取消一个 dispatch source
dispatch_source_cancel(timer);
});
//设置取消 dispatch source 时的处理,这个函数是异步执行的,取消之后就不会再有更多的 event 事件被传递,但是已经加入 queue 中的处理可以继续执行。
dispatch_source_set_cancel_handler(timer, ^{
NSLog(@"timer canceled");
});
//dispatch source 创建后是处于 suspend 状态的,在设置完 event handler 等属性之后需要调用 dispatch_resume 开启
dispatch_resume(timer);

Dispatch source 是不可重入的,任何在 dispatch source 处于 suspend 状态或者 event handler 正在执行的时候接收的事件都会被合并,并在 dispatch source resume 之后或者 handler 返回后被提交。

Dispatch Source 与 Dispatch Queue 不同的是它是可以取消的,而且可以设置取消时的 handler。

需要留意的一点是 dispatch source 在 suspend 状态下,如果直接设置 source = nil 或者重新创建 source 都会造成崩溃。正确的方式是在 resume 状态下调用dispatch_source_cancel后再重新创建。

其他

本文主要讨论了一些常用的 API,着重描述了使用时的一些注意点,容易导致问题的地方。阅读完会对iOS中使用 GCD 进行多线程编程有一个大概的认识。还有在一些其他情况下,诸如读取大文件等等也可使用 Dispatch I/O及 Dispatch Data 等,可以用时再去研究。

参考: