1.1 什么是GCD
Grand Central Dispatch是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想要执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的。隐藏可以统一管理。也可以执行任务。这样就比一切的线程更有效率。
1.2 多线程编程
线程
- 一个CPU执行的CPU命令列为一条无分叉路径,即为“线程”
定理
- 一个CPU核执行的CPU命令列为一条无分叉路径
-
这种无分叉路径不止一条,存在有多条时即为“多线程”。在多线程中,一个CPU核执行多条不同路径上的不同命令。
上下文切换
CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列。这种被称为“上下文切换”。
多线程编程
由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像1个CPU核能够并列地执行多个线程一样。而且在具有多个CPU核的情况下,就不是“看上去像了“,而是真的提供了多个CPU核并行执行多个线程的技术。
多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同的资源会导致数据的不一致(数据竞争),停止等待事件的线程会导致多个线程相互持续等待(死锁),使用太多线程会消耗大量内存等。
多线程的优点
弊端:应用程序在启动时,通过最先执行的线程,即”主线程“来描述用户界面,处理触摸屏幕的事件等。如果在该主线程中进行长时间的处理,如AR用画像的识别货数据库访问,就会妨碍主线程的执行(阻塞)。在OS X和iOS的应用程序中,会妨碍主线程中被称为RunLoop的主循环执行,从而导致不能更新用户界面,应用程序的画面长时间停滞等问题。
这就是长时间的处理不在主线程中执行而在其他线程中执行的原因。
使用多线程编程,在执行长时间的处理时间时仍可以保证用户界面的响应性能。
2. GCD的API
GCD的说明:开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。
2.1 Dispatch Queue
是执行处理的等待队列。应用程序编程人员通过dispatch_async函数等API,在Block语法中记述想执行的处理并将其追加到Dispatch Queue中。Dispatch Queue按照追加的顺序(FIFO)执行处理。
在执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Dispatch Queue,另一种是不等待现在执行中处理的Concurrent Dispatch Queue。
并行执行
就是使用多个线程同时执行多个处理
2.2 dispatch_queue_create
通过dispatch_queue_create函数可生成Dispatch Queue。
-
多个Serial Dispatch Queue 可并行执行
如果过多使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。
- 当想并行执行不发生数据竞争等问题的处理时,使用Concurrent Dispatch Queue。而且对于Concurrent Dispatch Queue来说,不管生成多少。由于XNU内核只使用有效管理的线程,因此不会发生Serial Dispatch Queue的那些问题。
2.3 Main Dispatch Queue / Global Dispatch Queue
第二种方法是获取系统标准提供的Dispatch Queue。
- Main Dispatch Queue
- 是在主线程中执行的Dispatch Queue,因为主线程只有一个,所以Main Dispatch Queue自然就是Serial Dispatch Queue串行队列。
-
追加到Main Dispatch Queue的处理在主线程的RunLoop中执行。由于在主线程中执行,因此要将用户界面的界面更新等一些必须在主线程中执行的处理追加到Main Dispatch使用。
- Global Dispatch Queue
- 另一个Global Dispatch Queue是所有应用程序都能够使用的Concurrent Dispatch Queue。没有必要通过dispatch_queue_create函数逐个生成Concurrent Dispatch Queue。只要获取Global Dispatch Queue使用即可。
- Global Dispatch Queue有四个优先级,分别是高优先级(High Priority),默认优先级(Default Priority),低优先级(Low Priority)和后台优先级(Background Priority)。
对于Main Dispatch Queue和Global Dispatch Queue执行dispatch_retain函数和dispatch_release函数不会引起任何变化,也不会有任何问题。这也是获取并使用Global Dispatch Queue比生成,使用,释放Concurrent Dispatch Queue更轻松的原因。
2.4 dispatch_set_target_queue
dispatch_queue_create函数生成的Dispatch Queue不管是Serial Dispatch Queue还是Concurrnet Dispatch Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。而变更生成的Dispatch Queue的执行优先级要使用dispatch_set_target_queue函数。
在必须将不可并行执行的处理追加到多个Serial Dispatch Queue中时,如果使用dispatch_set_target_queue函数将目标指定为某一个Serial Dispatch Queue,即可防止处理并行执行。
2.5 dispatch_after
- dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue,此源代码与在3秒后用dispatch_async函数追加Block到Main Dispatch Queue的相同。
- 因为Main Dispatch Queue在主线程的Runloop中执行,所以在此比如每隔1/60秒执行的Runloop中,Block最快在3秒后执行,最慢在3秒+1/60秒后执行,并且在Main Dispatch Queue有大量处理追加或主线程的处理本身有延时时,这个时间会更长。
- dispatch_time函数通常用于计算相对时间,而dispatch_walltime函数用于计算绝对时间。
2.6 Dispatch Group
在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理的需求。
- 向Global Dispatch Queue即Concurrent Dispatch Queue追加处理,多个线程并行执行,所以追加处理的执行顺序不定。执行时会发生变化,但是此执行结束的done一定是最后输出的。
- 无论向什么样的Dispatch Queue中追加处理,使用Dispatch Group都可以监听这些处理执行的结束。一旦检测到所有处理执行结束,就可以将结束的处理追加到Dispatch Queue中,这就是使用Dispatch Group的原因。
- 在Dispatch Group中也可以使用dispatch_group_wait函数仅等待全部处理执行结束。
2.7 dispatch_barrier_async
使用dispatch_barrier_async函数,dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束后,再将指定的处理追加到改Concurrent Dispatch Queue中,然后再有dispatch_barrier_async函数追加的处理执行完毕之后,Concurrent Dispatch Queue才恢复为一般的动作,追加到该Concurrent Dispatch Queue的处理又开始并行执行。
使用Concurrent Dispatch Queue和dispatch_barrier_async函数可实现高效的数据库访问和文件访问。
2.8 dispatch_sync
dispatch_async函数的“async”意味着“非同步”(asynchronous),就是将指定的Block“非同步”地追加到指定的Dispatch Queue中。dispatch_async函数不做任何等待。
dispatch_sync同步,(synchronous),即将指定的Block“同步”追加到指定的Dispatch Queue中。在追加Block结束之前,dispatch_sync函数会一直等待。
等待意味着当前线程停止
一旦调用dispatch_sync函数,那么在指定的处理执行结束之前,该函数不会返回。dispatch_sync函数可以简化源代码,也可说是简易版的dispatch_group_wait函数。
正因为dispatch_sync函数使用简单,所以容易引起问题,即死锁。
例如如果在主线程中执行以下源代码就会死锁
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"Hello?");
});
改源代码在Main Dispatch Queue即主线程中执行指定的Block,并等待其执行结束。而其实在主线程中正在执行这些源代码,所以无法执行追加到Main Dispatch Queue的Block。
下面的例子也一样
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
dispatch_sync(queue,^{
NSLog(@"Hello?");
});
});
Mian Dispatch Queue中执行的Block等待Main Dispatch Queue中要执行的Block执行结束。
当然Serial Dispatch Queue 也会引起相同的问题。
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue",NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"Hello?");
});
});
另外,由Dispatch_barrier_async函数中含有async可推出出,相应的也有dispatch_barrier_sync函数。dispatch_barrier_async函数的作用是在等待追加的处理全部执行结束后,再追加处理到Dispatch Queue中,此外,它还与dispatch_sync函数相同,会等待追加处理的执行结束。
2.9 dispatch_apple
dispatch_apple函数是Dispatch_sync函数和Dispatch Group的关联API。该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu",index);
});
NSLog(@"done");
结果如下:
1
2
3
0
4
5
6
7
8
9
done
因为在Global Dispatch Queue中执行处理,所以各个处理的时间不定。但是输出结果中最后的done必定在最后的位置上。这是因为dispatch_apple函数会等待全部处理执行结束。
NSArray *array = @[@0,@1,@2,@3,@4];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%zu:%@",index,[array objectAtIndex:index]);
});
NSLog(@"done");
结果:
3:3
0:0
1:1
2:2
4:4
done
这样可以简单地在Global Dispatch Queue中对所有的元素执行Block
另外,由于dispatch_apply函数也与dispatch_sync函数相同,会等待处理执行结束,因此推荐在dispatch_async函数非同步地执行dispatch_apply函数。
NSArray *array = @[@0,@1,@2,@3,@4];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 在Global Dispatch Queue中非同步执行
dispatch_async(queue, ^{
// 等待dispatch_apply函数中全部处理执行结束
dispatch_apply([array count], queue, ^(size_t index) {
// 并列处理包含在NSArray对象的全部对象
NSLog(@"%zu:%@",index,[array objectAtIndex:index]);
});
// dispatch_apply函数中的处理全部执行结束
// 在Main Dispatch Queue中非同步执行
dispatch_async(dispatch_get_main_queue(), ^{
// 在Main Dispatch Queue中执行处理用户界面更新等等
NSLog(@"done 界面刷新");
});
});
执行结果:
1:1
2:2
3:3
0:0
4:4
done 界面刷新
2.10 dispatch_suspend/dispatch_resume
当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。在这种情况下,只要挂起Dispatch Queue即可。当可以执行时再恢复。
- dispatch_suspend函数挂起指定的Dispatch Queue
dispatch_suspend(queue);
- Dispatch_resume函数恢复指定的Dispatch Queue
dispatch_resume(queue);
这些函数对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。而恢复则使这些处理能够继续执行。
2.11 Dispatch Semaphonre
如前所述,当并行执行的处理更新数据时,会产生数据不一致的情况,有时应用程序还会导致异常结束。虽然使用Serial Dispatch Queue和disaptch_barrier_async函数可以避免这类问题,但有必要进行更细粒度的为他控制。
以下场景,不考虑顺序,将所有数据追加到NSMutableArray中。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];;
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
因为该源代码使用Global Dispatch更新NSMutableArray类对象,所以执行后由内存错误导致应用程序异常结束的概率很高。此时应使用Dispatch Semaphore。
- Disaptch Semaphore
Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。所谓信号,类似于过马路时常用的手旗。可以通过时举起手旗,不可通过时放下手旗。而在Dispatch Semaphore中,使用计数来实现该功能。计数为零时等待,计数为1货大于1时,减去1而不等待。
dispatch_semaphore_wait函数返回0时,可安全地执行需要进行排他控制的处理。改处理结束时通过dispatch_semaphore_signal函数将Dispatch Semaphore额计数值加1.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 生成dispatch Semaphore,并且计数初始值设定为1.保证可访问NSMutableArray类对象的线程同时只能有1个
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
// 等待Dispatch Semaphore,一直等待,直到Dispatch Semaphore的计数值达到大于等于1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[array addObject:[NSNumber numberWithInt:i]];
// 排他控制处理结束,通过dispatch_semaphore_signal函数将Dispatch Semaphore的计数值加1
dispatch_semaphore_signal(semaphore);
});
}
在没有Serial Dispatch和dispatch_barrier_async函数那么大粒度并且一部分处理需要进行排他控制的情况下,Dispatch Semaphore便可发挥威力.
2.12 dispatch_once
dispatch_once函数是保证在应用程序中只执行一次指定处理的API。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//初始化操作
});
通过dispatch_once函数,该源代码即使在多线程环境下执行,也可以保证百分之百的安全。
2.13 Dispatch I/0
在读取较大文件时,如果将文件分成合适的大小并使用Global Dispatch Queue并列读取,会比一般的读取速度快很多。现今的输入/输出硬件已经可以做到一次使用多个线程更快地并列读取了。能实现这一功能的就是Dispatch I/0和Dispatch Data。
如果想提高文件读取的速度,可以尝试使用Dispatch I/O
3. GCD的实现
3.1 Dispatch Queue
GCD的Dispatch Que非常方便,它是如何实现的呢?
- 用于管理追加的Block得C语言层实现的FIFO队列
- Atomic函数中实现的用于排他控制的轻量级信号
- 用于管理现场的C语言层实现的一些容器
通常,应用程序中编写的线程管理用的代码要在系统级实现
系统级即iOS和OS X的核心XNU内核级上实现
3.2 Dispatch Source
GCD中除了注意的Dispatch Queue外,还有不太引人注目的Dispatch Source。它是BSD系内核惯有功能kqueue的包装。
Kqueue是在XNU内核中发生各种事件时,在应用编程方执行处理的技术。其CPU负荷非常小,尽量不占用资源。kqueue可以说是应用程序处理XNU内核中发生的各种事件的方法中最优秀的一种。
Dispatch Source与Dispatch Queue不同,是可以取消的。而且取消时必须执行的处理可指定为调用的Block形式。因此使用Dispatch Source实现XNU内核中发生的事件处理要比直接使用kqueue实现更为简单。在必须使用kqueue的情况下希望大家还是使用Dispatch Source,它比较简单。