GCD 概要
什么是 GCD
Grand Central Dispatch (GCD) 是异步执行任务的技术之一。 一般将应用程序中记述的线程管理用的代码在系统集中实现。开发者只需要定义相执行的任务并追加到适当的 Dispatch Queue 中,GCD 就能生成必要的线程并执行计划任务。由于线程管理是作为系统的一部分来实现的,因此可以统一管理,也可以执行任务,这样就比以前的线程更有效率。
dispatch_async (queue, ^{
/**
* 长时间处理
*
* 例如 AR 用画像识别
* 例如数据库访问
*/
.....
/*
* 长时间处理结束,主线程使用改处理结果
*/
dispatch_async (dispatch_get_main_queue(), ^{
/*
* 只在主线程可以执行的处理
*
* 例如用户刷新页面
*/
});
});
多线程编程
- 线程: 线程是指程序的一个指令执行序列
- 多线程:
- 上下文切换:
- 多线程编程:
多线程编程实际上是一种易发生各种问题的编程技术。
- 数据竞争
- 死锁
- 太多线程导致消耗大量内存
尽管极易发生各种问题,也应当使用多线程编程。因为使用多线程编程可保证应用程序的响应性能。
GCD 大大简化了偏于复杂的多线程编程的源代码。
GCD 的 API
Dispatch Queue
首先回顾下苹果官方对 GCD 的说明。
开发者要做的只是定义向执行的任务并追加到适当的 Dispatch Queue 中。
dispatch_async (queue, ^{
/*
* 想执行的任务
*/
});
Dispatch Queue是执行处理的等待队列。应用程序编程人员通过 dispatch_async 函数等 API ,在 Block 语法中技术想执行的处理并将其追加到 Dispatch Queue。 Dispatch Queue 按照追加的顺序 (先进先出 FIFO)执行处理。
Dispatch Queue 的种类 | 说明 |
---|---|
Serial Dispatch Queue | 使用一个线程,等待现在执行中处理结束 |
Concurrent Dispatch Queue | 使用多线程,不等待现在执行中处理结束 |
如何得到这些 Dispatch Queue 呢?有两种方法。
dispatch_queue_create
第一种方法
// Serial Dispatch Queue
dispatch_queue_t mySerialDispatchQueue =
dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
// Concurrent Dispatch Queue
dispatch_queue_t myConcurrentDispatchQueue =
dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myConcurrentDispatchQueue, ^{
NSLog(@"block on myConcurrentDispatchQueue");
});
//dispatch_release(myConcurrentDispatchQueue); //ARC 中不需要
Serial Dispatch Queue 生成个数的注意事项
- 只在为了避免多线程编程问题之一---数据竞争时使用 Serial Dispatch Queue
- 使用 dispatch_queue_create 可以生成任意多个 Dispatch Queue, 如果过多使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。
dispatch_queue_create 命名规则:推荐使用应用程序 ID 这种逆序全程域名。该名称现在 Xcode 的 Instruments 的调试器中作为 Dispatch Queue 的名称。另外这个名称海显示在 Crash Log 中。
Main Dispatch Queue / Global Dispatch Queue
第二种方法是获取系统标准提供的 Dispatch Queue
/*
* Main Dispatch Queue 的获取方法
*/
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
/*
* Global Dispatch Queue (高优先级) 的获取方法
*/
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
/*
* Global Dispatch Queue (默认优先级) 的获取方法
*/
dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* Global Dispatch Queue (低优先级) 的获取方法
*/
dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
/*
* Global Dispatch Queue (后台优先级) 的获取方法
*/
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
但是通过 XNU 内核用于 Global Dispatch Queue 的线程,不能保证实时性,因此执行优先级只是大致判断。
另外对于 Main Dispatch Queue 和 Global Dispatch Queue 执行 dispatch_retain 和 dispatch_release 并不会引起任何变化,也不会有任何问题。
/*
* 在默认优先级的 Global Dispatch Queue 中执行 block
*/
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
/*
* 可并行执行的处理
*/
/*
* 在 Main Dispatch Queue 中执行 block
*/
dispatch_async(dispatch_get_main_queue(), ^{
/*
* 只能在主线程中执行的处理
*/
});
});
dispatch_set_target_queue
dispatch_queue_create 函数生成的 Dispatch Queue 都使用与默认优先级的 Global Dispatch Queue 相同执行优先级的线程。而变更生成的 Dispatch Queue 的执行优先级要使用 dispatch_queue_create 函数。
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);
将 Dispatch Queue 指定为 dispatch_set_target_queue 函数的参数, 不仅可以变更 Dispatch Queue 的执行优先级,还可以作为 Dispatch Queue 的执行阶层。
dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"waited at least three seconds.");
});
dispatch_time 计算相对时间
dispatch_walltime 计算绝对时间
dispatch_time_t getDispatchTimeByDate(NSDate *date)
{
NSTimeInterval interval;
double second, subsecond;
struct timespec time;
dispatch_time_t milestone;
interval = [date timeIntervalSince1970];
subsecond = modf(interval, &second);
time.tv_sec = second;
time.tv_nsec = subsecond * NSEC_PER_SEC;
milestone = dispatch_walltime(&time, 0);
return milestone;
}
Dispatch Group
在最佳的 Dispatch Queue 中的多个处理全部结束想执行结束处理,这种情况会经常出现。在此种情况下使用 Dispatch Group,这些 Block 如果全部执行完毕,就会执行 Main Dispatch Queue 中结束处理用的 Block。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"blk0");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk2");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});
// 阻塞线程等待结束,
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_group_wait 函数的返回值不为0,就意味着虽然经过了指定的时间,但是属于 Dispatch Group 的某一个处理还在执行中。如果返回值为0,那么全部处理执行结束。
dispatch_barrier_sync
在访问数据或文件时,使用 Sirial Dispatch Queue 可避免数据竞争的问题。
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_sync(queue, blk_for_writing);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
dispatch_sync
dispatch_async 函数的 『async』意味着『非同步』(asynchronous),就是将指定的 Block 『非同步』地追加到指定的 Dispatch Queue 中。dispatch_sync 函数会一直等待。
dispatch_apply
把一项任务提交到队列中多次执行,具体是并行执行还是串行执行由队列本身决定.注意,dispatch_apply不会立刻返回,在执行完毕后才会返回,是同步的调用。
同 dispatch_sync 一样 也是同步的函数,会一直等待,直到所有的任务都完成。
dispatch_suspend / dispatch_resume
- dispatch_suspend : 挂起
- dispatch_resume : 恢复
Dispatch Semaphore
创建信号量,可以设置信号量的资源数。0表示没有资源,调用dispatch_semaphore_wait会立即等待。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
等待信号,可以设置超时参数。该函数返回0表示得到通知,非0表示超时。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
-
通知信号,如果等待线程被唤醒则返回非0,否则返回0。
dispatch_semaphore_signal(semaphore);
最后,还是回到生成消费者的例子,使用dispatch信号量是如何实现同步:
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //消费者队列
while (condition) {
if (dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_SEC))) //等待10秒
continue;
//得到数据
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //生产者队列
while (condition) {
if (!dispatch_semaphore_signal(sem))
{
sleep(1); //wait for a while
continue;
}
//通知成功
}
});
dispatch_once
static int initialized = NO;
if (initialized == NO) {
/*
* 初始化
*/
initialized = YES;
}
如果使用dispatch_once
static dispatch_once_t pred;
dispatch_once(&pred, ^{
/*
* 初始化
*/
});
使用 dispatch_once,在多线程模式中仍然安全。
常用于单例模式中生成单例对象。
GCD 术语
Serial vs. Concurrent 串行 vs. 并发
这些术语描述当任务相对于其它任务被执行,任务串行执行就是每次只有一个任务被执行,任务并发执行就是在同一时间可以有多个任务被执行。
Synchronous vs. Asynchronous 同步 vs. 异步
在 GCD 中,这些术语描述当一个函数相对于另一个任务完成,此任务是该函数要求 GCD 执行的。一个同步函数只在完成了它预定的任务后才返回。
一个异步函数,刚好相反,会立即返回,预定的任务会完成但不会等它完成。因此,一个异步函数不会阻塞当前线程去执行下一个函数。
注意——当你读到同步函数“阻塞(Block)”当前线程,或函数是一个“阻塞”函数或阻塞操作时,不要被搞糊涂了!动词“阻塞”描述了函数如何影响它所在的线程而与名词“代码块(Block)”没有关系。代码块描述了用 Objective-C 编写的一个匿名函数,它能定义一个任务并被提交到 GCD 。
译者注:中文不会有这个问题,“阻塞”和“代码块”是两个词。
Critical Section 临界区
就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很可能会变质(译者注:它的值不再可信)。
Race Condition 竞态条件
这种状况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。
Deadlock 死锁
两个(有时更多)东西——在大多数情况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。
Thread Safe 线程安全
线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary
就不是线程安全的,应该保证一次只能有一个线程访问它。
Context Switch 上下文切换
一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。
Queues 队列
GCD 提供有 dispatch queues 来处理代码块,这些队列管理你提供给 GCD 的任务并用 FIFO 顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。
所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行的访问它们。当你了解了调度队列如何为你自己代码的不同部分提供线程安全后,GCD的优点就是显而易见的。关于这一点的关键是选择正确类型的调度队列和正确的调度函数来提交你的工作。
在本节你会看到两种调度队列,都是由 GCD 提供的,然后看一些描述如何用调度函数添加工作到队列的例子。
Serial Queues 串行队列
串行队列中的任务一次执行一个,每个任务只在前一个任务完成时才开始。而且,你不知道在一个 Block 结束和下一个开始之间的时间长度,如下图所示:
Concurrent Queues 并发队列
在并发队列中的任务能得到的保证是它们会按照被添加的顺序开始执行,但这就是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。再说一遍,这完全取决于 GCD 。
Queue Types 队列类型
首先,系统提供给你一个叫做 主队列(main queue)
的特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发生消息给 UIView
或发送通知的。
系统同时提供给你好几个并发队列。它们叫做 全局调度队列(Global Dispatch Queues)
。目前的四个全局队列有着不同的优先级:background
、low
、default
以及 high
。要知道,Apple 的 API 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。
最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有五个队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。
以上是调度队列的大框架!
GCD 的“艺术”归结为选择合适的队列来调度函数以提交你的工作。体验这一点的最好方式是走一遍下边的列子,我们沿途会提供一些一般性的建议。
GCD 实现
Dispatch Queue
- 用于管理追加的 Block 的 C 语言层实现的 FIFO 队列
- Atomic 函数中实现的用于排他控制的轻量级信号
- 用于管理线程的 C 语言层实现的一些容器
通常,应用程序中编写的线程管理用的代码要在系统级实现
GCD 在系统级即 IOS 和 OSX 的核心 XNU 内核上实现。因此,无论编程人员如何努力编写管理线程的代码,在性能方面也不可能胜过 XNU 内核级所实现的 GCD。
组件名称 | 提供技术 |
---|---|
libdispatch | Dispatch Queue |
Libc (pthreads) | pthread_workqueue |
XNU 内核 | workqueue |
参考
- 《Objective-C高级编程:iOS与OS X多线程和内存管理》
- GCD 深入理解:第一部分