Grand Central Dispatch
3.1 Grand Central Dispatch(GCD)摘要
3.1.1 什么是GCD
Grand Central Dispatch(GCD)是异步执行的任务之一。
3.1.2 多线程编程
在Objective-C的if语句和for语句等控制语句或函数调用的情况下,执行命令列的地址会远离当前的位置(位置偏移)。但是,由于一个CPU一次只能执行一个命令列,不执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,其整不会出现分歧。
![](https://img.haomeiwen.com/i2735607/3cfd7e16a2abd333.png)
这里所说的“一个CPU执行的CPU命令列为一条无分叉路径”即为线程。这种无分叉路径不止一条,存在多条即为“多线程。”在多线程中,一个CPU核执行多条不同路径上的不同命令。
![](https://img.haomeiwen.com/i2735607/568aba25424c8ebf.png)
基本上一个CPU核一次能够执行的CPU命令始终为1.
多线程的问题:数据竞争、死锁、太多线程导致消耗大量内存等。
多线程可以保证应用程序的响应性能。应用程序启动时,通过最先执行的线程,即“主线程”来描绘用户界面、处理触摸屏幕的事件等。如果该主线程中进行长时间的处理,如AR用画像的识别或数据库访问,就会妨碍主线程德执行(阻塞)。在OSX和iOS的应用程序中,会妨碍主线程中被称为RunLoop的主循环的执行,从而导致不能更新用户界面,应用程序的书面长时间停滞等问题。
![](https://img.haomeiwen.com/i2735607/bf1b3fca807ee4a4.png)
使用多线程,在执行长时间的处理时仍可保证用户界面的响应性能。
3.2 GCD的API
3.2.1 Dispatch Queue
GCD官方说明:开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。
dispatch_async(queue, ^{
// 想要执行的任务
});
将Block通过dispatch_async
函数"追加"赋值在变量queue的“Dispatch Queue中”。Dispatch Queue按照追加的顺序(先进先出FIFO)执行处理
![](https://img.haomeiwen.com/i2735607/a2f2baf381c5c3cf.png)
在执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Disaptch Queue,另一种是不等待现在执行中处理的Concurrent Dispatch Queue。
Dispatch Queue 的种类 | 说明 |
---|---|
Serial Dispatch Queue | 等待现在执行中处理结束 |
Concurent Dispatch Queue | 不等待现在执行中处理结束 |
![](https://img.haomeiwen.com/i2735607/fa3f6c12e853dfef.png)
并行执行的处理数量取决于当前系统的状态。即 iOS 和 OSX 基于 Dispatch Queue 中的处理数、CPU核数以及CPU负荷等当前系统的状态来决定 Concurent Dispatch Queue 中并行执行的处理数。所谓“并行执行”,就是使用多个线程执行多个处理。
![](https://img.haomeiwen.com/i2735607/80c682e9098f88e3.png)
iOS 和 OSX 的核心————XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的内核。XNU内核仅使用 Concurent Dispatch Queue 便可完美地管理并行执行多个处理的线程。
3.2.2 dispatch_queue_create
通过dispatch_queue_create
函数可生成 Dispatch Queue。以下源代码生成了 Serial Dispatch Queue。
dispatch_queue_t serialQueue =
dispatch_queue_create("com.example.gcddemo.serialqueue", NULL);
在说明dispatch_queue_create
函数之前,先讲一下关于 Serial Dispatch Queue 生成个数的注意事项。
如前所述, Concurrent Dispatch Queue 并行执行多个追加处理,而 Serial Dispatch Queue 同时只能追加一个处理。虽然 Serial Dispatch Queue 和 Concurrent Dispatch Queue 受到系统资源的限制, 但用dispatch_queue_create
函数可生成任意多个 Dispatch Queue。
当生成多个 Serial Dispatch Queue 时,各个 Serial Dispatch Queue将并行执行。虽然在一个 Serial Dispatch Queue 中同时只能执行一个追加处理,但如果将处理分别追加到4个 Serial Dispatch Queue 中, 各个 Serial Dispatch Queue 执行一个,即为同时执行四个处理(串行异步)。
![](https://img.haomeiwen.com/i2735607/3cbf8660f7ee618b.png)
如果过多使用多线程,就会消耗大量内存,引发大量的上下文切换,大幅度降低系统的响应性能。
![](https://img.haomeiwen.com/i2735607/d5f769074e852f15.png)
只在为了避免多线程编程问题之一————多个线程更新想用资源所导致数据竞争时使用 Serial Dispatch Queue。
![](https://img.haomeiwen.com/i2735607/b5935e73fc7b4291.png)
继续说明dispatch_queue_create
函数,第一个参数为生成的 queue 的名称,推荐使用应用程序ID这种逆序全程域名(FQDN, fully qualified domain name),便于在调试和日志中定位问题。第二个参数,生成 Serial Dispatch Queue 时可为 DISPATCH_QUEUE_SERIAL
或 NULL, 生成 Concurent Dispatch Queue 时为DISPATCH_QUEUE_CONCURRENT
。
iOS6.0后,Dispatch Queue不再需要程序员负责释放(dispatch/_release)。
3.2.3 Main Dispatch Queue / Global Dispatch Queue
Main Dispatch Queue 是在主线程中执行的 Dispatch Queue。因为主线程只有一个,所以 Main Dispatch Queue 是 Serial Dispatch Queue。
追加到 Main Dispatch Queue 的处理在主线程的 RunLoop 中执行。由于在主线程中执行,因此要将用户界面的界面更新等一些必须在主线程中执行的处理追加到 Main Dispatch Queue 使用。这正好与NSObject类的 performSelectorOnMainThread
实例方法这一执行的方法相同。
![](https://img.haomeiwen.com/i2735607/084407ad12ca2e4c.png)
Global Dispatch Queue 是所有应用程序都能够使用的 Concurrent Dispatch Queue。需要时获取使用即可
Global Dispatch Queue 有4个优先级,但是通过 XNU 内核用于 Global Dispatch Queue 的线程并不能保证实时性,因此执行优先级只是大致的判断。
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 | 执行优先级:后台 |
3.2.4 dispatch_set_target_queue
dispatch_set_target_queue
是用来变更生成的 Dispatch Queue 的执行优先级的。
dispatch_set_target_queue(<#dispatch_object_t _Nonnull object#>, <#dispatch_queue_t _Nullable queue#>)
指定要变更执行优先级的 Dispatch Queue 为dispatch_set_target_queue
函数的第一个参数,指定与要使用的执行优先级相同的 Dispatch Queue 为第二个参数(目标)。
将 Dispatch Queue 指定为 dispatch_set_target_queue
函数的参数,不仅可以变更 Dispatch Queue 的执行优先级,还可以作成 Dispatch Queue 的执行阶层。如果多个 Serial Dispatch Queue 中使用dispatch_set_target_queue
函数指定目标为某一个 Serial Dispatch Queue ,那么原本应并行执行的多个 Serial Dispatch Queue ,在目标 Serial Dispatch Queue 上只能同时执行一个处理。
![](https://img.haomeiwen.com/i2735607/3651192fb81d5ad2.png)
在必须将不可并行执行的处理追加到多个 Serial Dispatch Queue 中时,如果使用dispatch_set_target_queue
函数将目标指定为某一个 Serial Dispatch Queue,即可防止处理并行执行。
3.2.5 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(), ^{
NSLog(@"waited after at least three seconds");
});
因为 Main Dispatch Queue 在主线程的 RunLoop 执行,所以在比如每隔1/60秒执行的 RunLoop 中,Block 最快在3秒后,最慢在3秒+1/60秒后执行,并且在 Main Dispatch Queue 有大量处理追加或主线程处理本身有延迟时,这个时间会更长。
第一个参数是指定时间用的dispatch_time_t
类型的值。该值使用dispatch_time
函数(计算相对时间)或dispatch_walltime
函数(计算绝对时间)。
3.2.6 Dispatch Group
当追加到的 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");
});
执行结果
blk0
blk1
blk2
done
在 Dispatch Group 中也可以使用 dispatch_group_wait
函数仅等待全部处理执行结束。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
// 属于 Dispatch Group 的全部处理执行结束
} else {
// 属于 Dispatch Group 的某一个处理还在执行
}
当dispatch_group_wait
函数返回的等待时间为DISPATCH_TIME_FOREVER
时,意味着永久等待,属于 Dispatch Group 的处理必定全部执行结束,因此返回值恒为0。
需要注意的是,一旦调用dispatch_group_wait
函数,该函数就处于调用的状态而不返回。即执行dispatch_group_wait
函数的现在的线程(当前线程)停止。在经过dispatch_group_wait
函数中指定的时间或属于指定 Dispatch Group 的处理全部执行结束之前,执行该函数的线程停止。
指定DISPATCH_TIME_NOW
,则不用任何等待即可判定属于 Dispatch Group 的处理是否执行结束。
3.2.7 dispatch_barrier_async
dispatch_queue_t queue = dispatch_queue_create("com.example.gcddemo.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(dataQueue, ^{
NSLog(@"read data 1");
});
dispatch_async(dataQueue, ^{
NSLog(@"read data 2");
});
dispatch_barrier_async(dataQueue, ^{
NSLog(@"write data 1");
});
dispatch_async(dataQueue, ^{
NSLog(@"read data 3");
});
dispatch_barrier_async
函数会等待之前追加到 Dispatch Queue 中的处理全部执行结束后,再将dispatch_barrier_async
函数指定的处理追加到 Dispatch Queue 中,然后在dispatch_barrier_async
函数追加的处理执行完毕后,继续执行dispatch_barrier_async
函数之后追加到 Dispatch Queue 的处理。
![](https://img.haomeiwen.com/i2735607/6dc4a561db15b99c.png)
使用 Concurrent Dispatch Queue 和 dispatch_barrier_async
函数可实现搞笑的数据库访问和文件访问。
3.2.8 dispatch_sync
dispatch_async
(异步),指定的 Block ”非同步“地追加到指定的 Dispatch Queue 中,dispatch_async
函数不做任何等待。
![](https://img.haomeiwen.com/i2735607/6615883516a48972.png)
dispatch_sync
(同步),将指定的Block”同步“追加到指定的 Dispatch Queue 中,在追加Block结束之前,dispatch_sync
函数会一直等待。
![](https://img.haomeiwen.com/i2735607/7cc75007ea5c58ac.png)
使用dispatch_sync
函数需要注意死锁问题。
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"123");
});
该源码在主线程中执行指定的 Block,并等待其执行结束,而其实在主线程中正在执行这些源代码,所以无法追加到主线程的 Block。
3.2.9 dispatch_apply
da函数是ds函数和 dg 的关联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");
执行结果
0
1
3
2
5
4
6
7
8
9
done
因为在 Global Dispatch Queue 中执行处理,所以各个处理的执行时间不定。但是输出结果最后的done必定在最后的位置上。这是因为da函数会等待全部处理执行结束。
由于da函数与ds函数相同,会等待处理执行结束,因此推荐在dispatch_async
函数中非同步地执行da函数。
对比for循环中使用dispatch_async
函数,并行的机制比串行的机制更安全、快速。
3.2.10 dispatch_suspend/dispatch_resume
当追加大量处理到 Dispatch Queue 时,在追加处理的过程中,有时希望不执行已追加的处理。这种情况可以使用dispatch_suspend
函数挂起指定的 Dispatch Queue,当可以执行时再使用dispatch_resume
函数恢复指定的 Dispatch Queue。
dispatch_suspend
函数不会立即停止正在执行的 Block,而是在当前 Block 执行完成后,暂停后续的 Block 执行。
3.2.11 Dispatch Semaphore
当并行执行的处理更新数据时,会产生数据不一致的情况,有时应用程序还会异常结束。虽然使用 Serial Dispatch Queue 和dispatch_barrier_async
函数可避免这类问题,但有必要进行更细粒度的排他控制,这时可以使用 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 array];
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函数
等待 Dispatch Semaphore 的计数值增加的线程
就由最先等待的线程执行。
*/
dispatch_semaphore_signal(semaphore);
});
}
NSLog(@"done");
3.2.12 dispatch_once
dispatch_once
函数式保证在应用程序执行中只执行一次指定处理的API。多线程环境下也可保证百分百安全。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// code to be executed once
});
3.2.13 Dispatch I/O
在读取较大文件时,如果将文件分成合适的大小并使用 Global Dispatch Queue 并列读取的话能提高效率。现今的输入/输出硬件已经可以做到一次使用多个线程更快地并列读取了。能实现这一功能的就是 Dispatch I/O 和 Dispatch Data。
通过Dispatch I/O读写文件时,使用 Global Dispatch Queue 将1个文件按某个大小 read/write。
dispatch_sync(queue, ^{/* 读取 0 ~ 8191 字节*/});
dispatch_sync(queue, ^{/* 读取 8192 ~ 16383 字节*/});
dispatch_sync(queue, ^{/* 读取 16384 ~ 24575 字节*/});
dispatch_sync(queue, ^{/* 读取 24576 ~ 32767 字节*/});
像上面这样,将文件分割为一块一块地进行读取处理。
以下为苹果中使用 Dispatch I/O 和 Dispatch Data 的示例。
pipe_q = dispatch_queue_create("PipeQ",NULL);
pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM,fd,pipe_q,^(int err){
close(fd);
});
*out_fd = fdpair[i];
dispatch_io_set_low_water(pipe_channel,SIZE_MAX);
dispatch_io_read(pipe_channel,0,SIZE_MAX,pipe_q, ^(bool done,dispatch_data_t pipe data,int err){
if(err == 0)
{
size_t len = dispatch_data_get_size(pipe data);
if(len > 0)
{
const char *bytes = NULL;
char *encoded;
dispatch_data_t md = dispatch_data_create_map(pipe data,(const void **)&bytes,&len);
asl_set((aslmsg)merged_msg,ASL_KEY_AUX_DATA,encoded);
free(encoded);
_asl_send_message(NULL,merged_msg,-1,NULL);
asl_msg_release(merged_msg);
dispatch_release(md);
}
}
if(done)
{
dispatch_semaphore_signal(sem);
dispatch_release(pipe_channel);
dispatch_release(pipe_q);
}
});
dispatch_io_create
函数生成 Dispatch I/O,并指定发生错误时用来执行处理的 Block,以及执行该 Block的 Dispatch Queue。dispatch_io_set_low_water
函数设定一次读取的大小(分割大小),dispatch_io_read
函数使用 Global Dispatch Queue 开始并列读取。每当各个分割的文件快读取结束时,将含有文件块数据的 Dispatch Data 传递给dispatch_io_read
函数指定的读取结束时回调用的 Block。回调用的 Block 分析传递过来的 Dispatch Data 并进行结合处理。
如果想提高文件读取速度,可以尝试使用Dispatch I/O。
网友评论