一 什么是GCD
Grand Central Dispatch(GCD) 是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统一部分来实现的,因此可以统一管理,也可执行任务,这样就比以前的线程更有效率,虽然NSOperation是基于GCD封装,但是由于NSOperation的功能更多(添加依赖,优先级等),所以比NSOperation也要更效率一些。
也就是说,GCD用我们难以置信非常简洁的记述方法,实现了极为复杂繁琐的多线程编程,可以说这是一项划时代的记述。下面是用了GCD源代码的例子,虽然稍显抽象,但也能从中感受到GCD的威力。
dispatch_queue_t queue = dispatch_queue_create("queue1", NULL);
dispatch_async(queue, ^{
/*
* 长时间处理
* 例如AR用画像识别
* 例如数据库访问
* 例如图像处理压缩
*/
/*
* 长时间处理结束,主线程使用该处理结果
*/
dispatch_async(dispatch_get_main_queue(), ^{
/*
* 只在主线程可以执行的处理
*例如UI刷新操作
*/
});
});
上面的就是在后台线程中执行长时间处理,处理结束时,主线程使用该处理结果的源代码。
二,多线程的弊端
1,多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同的资源会导致数据的不一致(数据竞争,需要加锁、同步、dispatch_barrier_async、Dispatch SemaPhore等办法来避免这一问题,后面会有讲到)
2,停止等待的事件的线程会导致多个线程相互持续等待(死锁)
3,使用太多线程会消耗大量内存
尽管有各种弊端问题,也应当使用多线程编程,这是为什么呢?因为使用多线程编程可保证应用程序的响应性能,使得应用更加流畅。
三,GCD的API
1,Dispatch Queue
Dispatch Queue是执行处理的等待的队列,开发人员通过dispatch_async 函数等API,在Block语法中记述想执行的处理并将其追加到Dispatch Queue中。Dispatch Queue 按照追加的顺序(先进先出FIFO)执行处理。
(1),Dispatch Queue 的种类
Serial Dispatch Queue 串行队列,等待现在执行中处理结束
Concurrent Dispatch Queue 并行队列,不等待现在执行中处理结束
(2),Dispatch Queue 的生成
1,使用dispatch_queue_create生成一个queue
dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t _Nullable attr)
函数第一个参数指定Dispatch Queue的名称,第二个参数指定Dispatch Queue的类型,当为NULL时为串行队列,当参数为DISPATCH_QUEUE_CONCURRENT为并行队列(注意:当为串行队列时,创建多个队列的话会并行执行,会同时创建多个线程,如果生成2000个Serial Dispatch Queue ,那么就生成2000个线程,像之前提的弊端一样,如果过多使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能)
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);//串行队列
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", DISPATCH_QUEUE_CONCURRENT);//并行队列
2,使用系统提供的Main Dispatch Queue/Global Dispatch Queue
dispatch_queue_t queue = dispatch_get_main_queue();//获取主线程串行队列
dispatch_get_global_queue(long identifier, unsigned long flags)//identifier为优先级,而flags作为保留字段备用(一般为0),这个全局队列为并行队列
以下例举使用了Main Dispatch Queue 和Global Dispatch Queue 源代码:
/*
* 在默认优先级的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(), ^{
/*
* 只能在主线程执行的处理
*/
});
});
2,dispatch_set_target_queue
dispatch_queue_create函数生成的Dispatch Queue 不管是Serial Dispatch Queue 还是 Concurrent Dispatch Queue,都使用与默认优先级Global Dispatch Queue 相同执行优先级的线程,而要变更生成的Dispatch Queue 的执行优先级要使用 dispatch_set_target_queue 函数。在后台生成执行动作的处理的Serial Dispatch Queue 的生成方法如下:
dispatch_queue_t mySerialDispatchQueue1 = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue1", NULL);
dispatch_queue_t mySerialDispatchQueue2 = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue2", NULL);
dispatch_queue_t mySerialDispatchQueue3 = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t mySerialDispatchQueue4 = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue4", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_set_target_queue(mySerialDispatchQueue1, globalDispatchQueueBackground);//mySerialDispatchQueue为要变更的优先级,globalDispatchQueueBackground为要变更目标优先级,第一个参数不可使用Main Dispatch Queue 与 Global Dispatch Queue。
dispatch_set_target_queue(mySerialDispatchQueue2, mySerialDispatchQueue1);
dispatch_set_target_queue(mySerialDispatchQueue3, mySerialDispatchQueue1);
dispatch_set_target_queue(mySerialDispatchQueue4, mySerialDispatchQueue1);
dispatch_async(mySerialDispatchQueue1, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_async(mySerialDispatchQueue2, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
dispatch_async(mySerialDispatchQueue3, ^{
NSLog(@"3----%@",[NSThread currentThread]);
});
dispatch_async(mySerialDispatchQueue4, ^{
NSLog(@"4----%@",[NSThread currentThread]);
});
在必须将不可并行执行的处理追加到多个Serial Dispatch Queue 中时,如果使用dispatch_set_target_queue函数将目标指定为某一个Serial Dispatch Queue,即可防止处理并行执行。animation[13887:1088322] 1----<NSThread: 0x6000000792c0>{number = 3, name = (null)}
animation[13887:1088322] 2----<NSThread: 0x6000000792c0>{number = 3, name = (null)}
animation[13887:1088322] 3----<NSThread: 0x6000000792c0>{number = 3, name = (null)}
animation[13887:1088322] 4----<NSThread: 0x6000000792c0>{number = 3, name = (null)}
3,dispatch_after
经常会有这样的情况:想在3秒后执行处理。可能不仅限于3秒,总之这种想在指定时间后处理的情况,可使用dispatch_after函数来实现。
在3秒后将制定的Block追加到Main Dispatch Queue 中的源代码如下:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
/*
*需要注意的是,dispatch_after函数饼不是在指定时间后处理,因为Main Dispatch Queue 在主线程的RunLoop中执行,所以在比如每隔1/60秒执行的RunLoop中,block最快在3秒后执行,最慢在3秒+1/60秒执行。并且在Main Dispatch Queue有大量的处理追加或主线程的处理本身有延迟,这个时间会更长
*/
NSLog(@"waited at least three seconds ");
});
4,Dispatch Group
在追加到Dispatch Queue 中的多个处理全部结束后想执行结束处理。只使用一个Serial Dispatch Queue 时,只要将向执行的处理全部欧追加到该Serial Dispatch Queue 中并在最后追加结束处理,即可实现。但是在使用Concurrent Dispatch Queue时或同时使用多个Dispatch Queue 时,此种情况下使用Dispatch Group,源代码如下:
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");
});
结果如下:
gcd[28794:2250642] blk0
gcd[28794:2250640] blk1
gcd[28794:2250646] blk2
gcd[28794:2250554] done
5,dispatch_barrier_async
在访问数据库或文件时,使用Serial Dispatch Queue可避免数据竞争的问题。写入处理确实不可与其他写入处理以及包含数据处理的其他某些处理并行执行,但是如果读取处理只是与读取处理并行执行,那么多个并行执行就不会发生问题。
也就是说,为了高效的进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在任何一个读取处理没有执行的状态下,追加到Serial Dispatch Queue 中即可。
虽然利用Dispatch Group 和dispatch_set_target_queue函数也可实现,但是源代码会很复杂。所以需要使用dispatch_barrirt_async函数。看下源代码:
__block NSInteger count = 4;
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);//不可使用dispatch_get_global_queue
dispatch_async(queue, ^{
NSLog(@"blk0_for_reading,%ld",count);
});
dispatch_async(queue, ^{
NSLog(@"blk1_for_reading,%ld",count);
});
dispatch_async(queue, ^{
NSLog(@"blk2_for_reading,%ld",count);
});
dispatch_async(queue, ^{
NSLog(@"blk3_for_reading,%ld",count);
});
dispatch_barrier_sync(queue, ^{
count = 777;
NSLog(@"blk_for_writing");
});
dispatch_async(queue, ^{
NSLog(@"blk4_for_reading,%ld",count);
});
dispatch_async(queue, ^{
NSLog(@"blk5_for_reading,%ld",count);
});
dispatch_async(queue, ^{
NSLog(@"blk6_for_reading,%ld",count);
});
使用Concurrent Dispatch Queue 和 dispatch_barrier_async函数即可实现高效率的数据库访问,和文件访问。防止数据竞争
6,dispatch_async
dispatch_async函数就是非同步,就是将指定的block追加到指定的Dispatch Queue 中,dispatch_async函数不做任何等待。
dispatch_sync函数就是同步,就是将指定的block追加到指定的Dispatch Queue 中,dispatch_sync函数会一直等待。
dispatch_sync函数使用简单,所以也容易引起问题,即死锁。
例如如果在主线程中执行一下源代码就会死锁。
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"hello");
});
该源代码在Main Dispatch Queue即主线程中执行指定的Blobk,并等待其执行结束,而其实在主线程中正在执行这些源代码,所以无法执行追加到的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");
})
});
7,dispatch_apply
dispatch_apply函数是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(@"%ld",index);
});
NSLog(@"done");
结果如下:
gcd[30842:2662307] 0
gcd[30842:2662571] 1
gcd[30842:2662622] 2
gcd[30842:2662572] 3
gcd[30842:2662307] 4
gcd[30842:2662571] 5
gcd[30842:2662622] 6
gcd[30842:2662572] 7
gcd[30842:2662307] 8
gcd[30842:2662571] 9
gcd[30842:2662307] done
8,dispatch_suspend/dispatch_resume
当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理,例如演算结果被Block截获时,一些处理会对这个演算结果造成影响。在这种情况下只要挂起Dispatch Queue 即可,当可以时再恢复。
dispatch_suspemd函数挂起指定的Dispatch Queue
dispatch_suspend(queue);
dispatch_resume函数恢复指定的Dispatch Queue
dispatch_resume(queue);
这些函数对已经成执行的处理没有影响。挂起后追加到Dispatch Queue中但尚未执行的处理在此之后停止执行,而恢复则使德这些处理能继续执行。
9,Dispatch Semaphore
Dispatch Semaphore是持有计数的信号,该计数时多线程编程中的计数类型的信号。所谓信号,类似于过马路时常用的手旗,可以通过时举起手旗,不可通过时放下手旗,而在Dispatch Semaphore 中,使用计数来实现该功能,计数为0时等待,计数为1或大于1时,减去1而不等待。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* 生成Dispatch Semaphore
* Dispatch Semaphore的计数初始值设定为1
* 保证可访问NSMutableArray类的线程同时只有一个
*/
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);
/*
* 由于Dispatch Semaphore的计数值达到大于等于1
* 所以将Dispatch Semaphore 的计数值减去1
* dispatch_semaphore_wait 函数执行返回
* 即执行到此时的
* Dispatch Semaphore 的计数值恒为0
*
* 由于访问NSMutableArray类对象的线程只有1个
* 因此可安全的进行更新
*/
[array addObject:[NSNumber numberWithInt:i]];
/*
* 排他控制处理结束
* 所以通过dispatch_semaphore_signal 函数
* 将Dispatch Semaphore 的计数值加1
*
* 如果有通过dispatch_semaphore_wait函数等待ispatch Semaphore
* 计数值增加的线程
* 就由最先等待的线程执行
*
*/
dispatch_semaphore_signal(semaphore);
});
}
10,dispatch_once
dispatch_once函数是保证应用程序执行中只执行一次的指定处理的API。一般在单利模式上经常用到,来保证线程安全。下面示例源代码如下:
static dispatch_once_t pred;
dispatch_once(&pred, ^{
/*
* 初始化,保证在多核CPU下线程安全,在单利模式下使用。
*/
});