Grand Central Dispatch(GCD)概要
什么是GCD?
苹果官方这么描述的:Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级别中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue
中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。
看一个GCD的例子:
dispatch_async(queue, ^{
/*
* 长时间处理,比如数据库访问等等操作
*/
//长时间处理结束,主线程使用该处理结果
dispatch_async(dispatch_get_main_queue(), ^{
/*
* 只在主线程可以执行的处理
* 比如用户界面更新
*/
});
});
GCD的API
Dispatch Queue
开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue
中。如下:
dispatch_async(queue, ^{
//想要执行的任务
});
该源代码使用Block语法定义想执行的任务,通过dispatch_async
函数“追加”赋值在变量queue的Dispatch Queue
中。这样就可以使指定的Block再另一线程中执行。
Dispatch Queue
是执行处理的等待队列。🐶程序员可以通过dispatch_async
函数等API,在Block语法中记述想执行的处理并将其追加到Dispatch Queue
中,Dispatch Queue
按照追加的顺序(先进先出FIFO)执行处理。
在执行处理时有两种Dispatch Queue
,一种是等待现在执行中处理的Serial Dispatch Queue
,另一种是不等待现在执行中处理的Concurrent Dispatch Queue
。
Dispatch �Queue 的种类 | 说明 |
---|---|
Serial Dispatch Queue | 等待现在执行中处理结束 |
Concurrent Dispatch Queue | 不等待现在执行中处理结束 |
看一下下面这段代码:
dispatch_async(queue, blk0);
dispatch_async(queue, blk1);
dispatch_async(queue, blk2);
dispatch_async(queue, blk3);
dispatch_async(queue, blk4);
dispatch_async(queue, blk5);
dispatch_async(queue, blk6);
dispatch_async(queue, blk7);
当变量queue
为Serial Dispatch Queue
时,需要等待现在执行中的处理结束,所以首先执行blk0,blk0执行结束之后,接着执行blk1,blk1结束后再执行blk3,如此重复。同时执行的处理数只能有1个。所以执行改代码一定按照blk0、blk1、blk2、blk3、blk4、blk5、blk6、blk7的顺序执行。
当变量queue
为Concurrent Dispatch Queue
时,因为不用等待现在执行中的处理结束,所以首先执行blk0,不管blk0是否结束,都开始执行后面的blk1,不管blk1的执行是否结束,都开始执行后面的blk2,如此重复循环。虽然这样不用等待处理结束,可以并行执行多个处理,但是并行的处理数量取决于当前系统的状态。即iOS和OS X基于Dispatch Queue中的处理数、CPU核数以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue
中并行执行的处理数。并行执行就是使用多个线程执行多个处理。前面的源代码的一种执行情况:
线程0 | 线程1 | 线程2 | 线程3 |
---|---|---|---|
blk0 | blk1 | blk2 | blk3 |
blk4 | blk6 | blk5 | |
blk7 |
首先blk0在线程0中开始执行,接着blk1在线程1中,blk2在线程2中、blk3在线程3中开始执行。线程0中blk0执行结束后开始执行blk4,由于线程1中blk1还没有结束,此时线程2中的blk2执行结束,线程2开始执行blk5,线程1中blk1执行结束,线程1开始执行blk6,如此重复。像这样在Concurrent Dispatch Queue
中执行处理时,执行顺序会根据处理内容和系统状态发生改变。
看一下Serial Dispatch Queue
、Concurrent Dispatch Queue
和线程的关系:
dispatch_queue_create
这是获得Dispatch Queue
的第一种方法,通过dispatch_queue_create
函数可生成Dispatch Queue
。看一个示例:
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("cn.flametao.www.gcd.mySerialDispatchQueue", NULL);
dispatch_queue_create
函数的第一个参数指定Dispatch Queue
的名称,推荐使用应用程序ID这种逆序全程域名(FQDN,fully qualified domain name)。该名称会出现在调试过程中或程序崩溃时所产生的CrashLog中。第二个参数指定为NULL
时,生成的是Serial Dispatch Queue
,指定为DISPATCH_QUEUE_CONCURRENT
时,生成的是Concurrent Dispatch Queue
。像下面源代码一样:
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("cn.flame.www.gcd.myConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_create
返回值为表示Dispatch Queue
的dispatch_queue_t
类型。
现在来说一说关于生成Serial Dispatch Queue
个数的注意事项,如前所述,Concurrent Dispatch Queue
并行执行多个追加处理,Serial Dispatch Queue
同时只能处理1个追加处理。虽然这两者都受到系统资源限制,但是dispatch_queue_create
函数可生成任意多个Dispatch Queue
。
当生成多个Serial Dispatch Queue
时,各个Serial Dispatch Queue
在一起将并行执行,虽然每一个Serial Dispatch Queue
中同时只能执行一个追加处理。图示:
过多的使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅降低系统的响应性能。
使用Serial Dispatch Queue
避免多线程编程问题时的数据竞争问题:
Dispatch Queue
没有像Block
那样具有作为Objective-C
对象来处理的技术,所以没办法通过ARC来自动管理内存,只能由程序员负责释放。通过dispatch_queue_create
函数生成的Dispatch Queue
在使用结束后通过dispatch_release
函数释放。
dispatch_release(mySerialDispatchQueue);
看一下这样的代码:
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("cn.flametao.www.gcd.myConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myConcurrentDispatchQueue, ^{NSLog(@"block on myConcurrentDispatchQueue");});
dispatch_release(myConcurrentDispatchQueue);
该源代码中,在dispatch_async
函数中追加Block到Concurrent Dispatch Queue
中,并立即通过dispatch_release
函数进行释放是否可以呢?是没有问题的!Block通过dispatch_async
函数持有Dispatch Queue
。无论是Serial Dispatch Queue
还是Concurrent Dispatch Queue
都一样。当Block执行结束,才通过dispatch_release
函数释放该Block持有的Dispatch Queue
。
Main Dispatch Queue/Global Dispatch Queue
第二种方法是获得系统标准提供的Dispatch Queue
。
系统为我们提供了几个Dispatch Queue
,就是Main Dispatch Queue
和Global Dispatch Queue
。
Main Dispatch Queue
顾名思义,就是在主线程中执行的Dispatch Queue
。因为主线程只有一个,所以Main Dispatch Queue
是Serial Dispatch Queue
。追加到Main Dispatch Queue
的处理在主线程的RunLoop中执行。
Global Dispatch Queue
是所有应用程序都能够使用的的Concurrent Dispatch Queue
。没有必要通过dispatch_queue_create
函数逐个生成Concurrent Dispatch Queue
,只需要获取Global Dispatch Queue
使用即可。Global Dispatch Queue
有4个执行优先级:
- 高优先级(High Priority)
- 默认优先级(Default Priority)
- 低优先级(Low Priority)
- 后台优先级(Background Priority)
系统提供的Dispatch Queue
总结:
名称 | Dispatch Queue 的种类 | 说明 |
---|---|---|
Main Dispatch Queue | Serial Dispatch Queue | 主线程执行 |
Global Dispatch Queue(High Priority) | Concurrent Dispatch Queue | 执行优先级:高(最高优先) |
Global Dispatch Queue(Default Priority) | Concurrent Dispatch Queue | 执行优先级:默认 |
Global Dispatch Queue(Low Priority) | Concurrent Dispatch Queue | 执行优先级:低 |
Global Dispatch Queue(Background Priority) | Concurrent Dispatch Queue | 执行优先级:后台 |
各种Dispatch Queue
获取方法:
//Main Dispatch Queue
dispatch_queue_t mainDispatchQeuue = dispatch_get_main_queue();
//Global Dispatch Queue(High Priority)
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//Global Dispatch Queue(Default Priority)
dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//Global Dispatch Queue(Low Priority)
dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
//Global Dispatch Queue(Default Priority)
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
对Main Dispatch Queue
和Global Dispatch Queue
执行dispatch_retain
函数和dispatch_release
函数不会起任何作用。
dispatch_set_target_queue
dispatch_queue_create
函数生成的Dispatch Queue
不管是Serial Dispatch Queue
还是Concurrent Dispatch Queue
,都使用与默认优先级Global Dispatch Queue
相同执行优先级的线程。可以通过diapatch_set_target_queue
函数改变优先级。比如,在后台执行Serial Dispatch Queue
:
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("cn.flametao.www.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 Queue
。第一个参数不可以是Main Dispatch Queue
或者是Global Dispatch Queue
。
不仅仅可以改变Dispatch Queue
执行优先级,还可以作为Dispatch Queue
的执行层。如果在多个Serial Dispatch Queue
中用dispatch_set_target_queue
函数指定某一个Serial Dispatch Queue
,原本应该并行执行的多个Serial Dispatch Queue
,在目标Serial Dispatch Queue
上只能执行一个处理。因此可以利用这个防止处理并行执行。
dispatch_after
dispatch_after
函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue
。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
...
});
第一个参数是指定时间用的dispatch_time_t
类型的值。该值使用dispatch_time
或dispatch_walltime
制成。
第二个参数是指定要追加处理的Dispatch Queue
。
第三个参数是指定记述要执行处理的Block。
可以使用NSEC_PER_MSEC
计算毫秒,"ull"是C语言的数值字面量,是显示表明类型时使用的字符串(表示“unsigned long long”)。
dispatch_walltime
函数由POSIX种使用的struct timespec类型的时间得到dispatch_time_t
类型的值。dispatch_time
函数通常计算相对时间,dispatch_walltime
函数用于计算绝对时间。struct timespec类型的可以通过NSDate作成:
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
中的多个处理全部结束后想执行结束处理。当只有一个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, ^{//blk0});
dispatch_group_async(group, queue, ^{//blk1});
dispatch_group_async(group, queue, ^{//blk2});
dispatch_group_notify(goup, dispatch_get_main_queue(), ^{//blk3});
blk3一定是最后执行的,前面顺序由于多线程并行执行所以不确定。dispatch_group_async
函数与dispatch_async
函数相同,都追加Block到指定的Dispatch Queue
中。dispatch_group_async
函数指定生成的Dispatch Group
为第一个参数,指定的Block属于指定的Dispatch Group
。在追加到Dispatch Group
中的处理全部执行结束时,dispatch_group_notify
函数会将执行的Block追加到Dispatch Queue
中。
dispatch_group_wait
函数仅等待全部处理执行结束。返回值不为0就意味着虽然经过了指定的时间,但属于Dispatch Group
的某个处理还在执行中。如果返回0,那么全部处理执行结束。当等待时间为DISPATCH_TIME_FOREVER
必定返回0。指定为DISPATCH_TIME_NOW
则不用任何等待即可判断属于Dispatch Group
的处理是否执行结束。不过,推荐使用dispatch_group_notify
函数追加结束处理,可以简化代码。
和之前一样,create出来的需要手动释放:
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, ^{//blk0});
dispatch_group_async(group, queue, ^{//blk1});
dispatch_group_async(group, queue, ^{//blk2});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
dispatch_barrier_async
学过《操作系统》课程的都应该知道读写者问题
dispatch_queue_t queue = dispatch_queue_create("cn.flametao.www.gcd.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_reading);
dispatch_async(queue, blk1_reading);
dispatch_async(queue, blk2_reading);
dispatch_async(queue, blk3_reading);
dispatch_barrier_async(queue, blk_writing);
dispatch_async(queue, blk4_reading);
dispatch_async(queue, blk5_reading);
dispatch_async(queue, blk6_reading);
dispatch_async(queue, blk7_reading);
blk_writing之前的追加到Concurrent Dispatch Queue
的处理全部执行完,才执行blk_writing,执行完blk_writing,在接着执行后面追加的处理。可能的执行情况:
使用Concurrent Dispatch Queue
和dispatch_barrier_async
函数可实现高效率的数据库访问和文件访问。
dispatch_sync
dispatch_async
函数的async
意味着“非同步”,就是将指定的Block“非同步”地追加到指定的Dispatch Queue
中。dispatch_async
不做任何等待。
dispatch_sync
函数的sync
意味着“同步”,也就是将制定的Block“同步”追加到指定的Dispatch Queue
中。在追加的Block结束之前,dispatch_sync
函数会一直等待。
同dispatch_group_wait
函数说明所示,“等待”意味着当前线程停止。
学过汇编的应该知道,当主程序Call一个函数还没有返回的时候,CS、IP寄存器就没恢复到Call之前的状态,也就没办法继续主程序。dispatch_sync
很容易造成死锁。
例如:在主线程中执行以下源代码就会死锁:
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{...});
该源代码想在主线程中执行指定的Block,并等待Block运行结束,而追加的Block放在了Main Dispatch Queue
的最后(按照FIFO),主线程此时在等待dispatch_sync
返回,而dispatch_sync
在等待主线程执行完Block,从而死锁。图解:
只要dispatch_sync
的追加处理的queue为当前queue(并行的除外),会产生死锁。
dispatch_apply
dispatch_apply(size_t iterations, dispatch_queue_t _Nonnull queue, ^(size_t)block)
dispatch_apply
函数是dispatch_sync
和Dispatch Group
的关联API。该函数按指定的次数将制定的Block追加到指定的Dispatch Queue
中,并等待全部处理执行结束。第一个参数为重复次数,第二个参数为追加对象的Dispatch Queue
,第三个参数为追加的处理,是一个带有参数的Block,按第一个参数重复追加Block并区分各个
Block而使用,例如对NSArray类对象的所有元素执行处理时不需要一个一个编写for循环部分。
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");
一种执行结果:
0
1
2
3
4
5
6
9
7
8
done
因为在Global Dispatch Queue
中执行处理,所以各个处理的执行时间不定。但是输出结果“done”必定在最后位置。因为dispatch_apply
函数会等待全部处理执行结束。
由于dispatch_apply
函数也与dispatch_sync
函数相同,会等待处理执行结束。因此推荐在dispatch_async
函数中非同步执行dispatch_apply
函数。
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]);
});
//dispatch_apply中的处理全部执行结束
//在Mian Dispatch Queue中非同步执行
dispatch_async(dispatch_get_main_queue(), ^{
//...
});
});
dispatch_suspend/dispatch_resume
dispatch_suspend
函数挂起指定的Dispatch Queue
dispatch_suspend(queue);
dispatch_resume
函数恢复指定的Dispatch Queue
dispatch_resume(queue);
这些函数对已经执行的处理没有影响,挂起后,追加到Dispatch Queue
中尚未执行的处理在此之后停止执行直到恢复。
Dispatch Semaphore
和操作系统中的信号量机制类似,解决多线程中的数据不一致问题。之前说的Serial Dispatch Queue
和dispatch_barrier_async
也可简单解决这个问题,但有必要进行更细的排他控制。
Dispatch Semaphore
是持有计数的信号,该计数是多线程编程中的计数类型信号。计数为0是等待,计数大于0时,减去1而不等待。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
参数表示计数的初始值(效果等同同时访问的线程数量)。通前面一样“create”出来的必须通过dispatch_release
释放。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait
函数等待Dispatch Semaphore
的技术支持大于等于1。当计数值大于等于1,或者在待机中计数值大于等于1时,对该计数进行减法并从dispatch_semaphore_wait
函数返回。第二个参数通dispatch_group_wait
函数相同,有dispatch_time_t
类型值指定等待时间。
dispathc_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_semaphore_wait(semaphore, time);
if (result == 0) {
/*
* 由于Dispatch Semaphore的计数值达到大于或等于1
* 或者在待机中的指定时间内计数值达到大于或等于1
* 所以Dispatch Semaphore的计数值减去1
* 可安全的执行需要进行排他控制的处理
*/
} else {
/*
* 由于 Dispatch Semaphore 的计数值为0
* 因此在达到指定时间为止待机
*/
}
处理结束时通过dispatch_semaphore_signal
函数将Dispatch Semaphore
的计数加1。
dispatch_once
dispatch_once
函数保证在应用程序执行中只执行一次指定处理的API。举个初始化的例子
static int once = NO;
if (once == NO) {
// 初始化
once == YES;
}
// 如果使用dispatch_once,则为:
static dispatch_once_t once;
dispatch_once(&once, ^{
// 初始化
});
两者看起来没多大区别,但是通过dispatch_once
函数使在多线程环境中也可保证百分百安全。
注:参考书籍《Objective-C高级编程》