GCD 的 API
Dispatch Queue
串行(Serial):任务按顺序执行,一次只能执行一个任务,只有当前任务执行完毕后,才能执行下一个任务。
并发(Concurrent):任务的执行不需要等待上一个任务执行完后再执行。
并发(Concurrent)和并行(Parallel)的区别
简单点说就是:并发是指同一时间段内能够处理多个任务,如果在单核 CPU 上,是交替地执行任务;并行是指同一时刻处理多个任务。并行需要多核 CPU 的支持,是并发的一个子集。
如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。
在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。
我相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。
dispatch_set_target_queue
作用一:可以改变原队列的优先级,使之与目标队列的优先级一致。
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lincoln.MyConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"myConcurrentQueue");
});
dispatch_queue_t highGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 第一个参数是源队列,第二个参数时目标队列
dispatch_set_target_queue(concurrentQueue, highGlobalQueue);
dispatch_queue_create 生成的队列,不管是串行队列还是并发队列,都使用 Global Queue 的默认优先级。
m
作用二:设置队列的层级体系。比如让多个串行队列和并发队列统一在同一个串行队列中串行执行。
dispatch_queue_t targetQueue = dispatch_queue_create("com.lincoln.targetQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t firstQueue = dispatch_queue_create("com.lincoln.firstQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t secondQueue = dispatch_queue_create("com.lincoln.secondQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(firstQueue, targetQueue);
dispatch_set_target_queue(secondQueue, targetQueue);
dispatch_async(firstQueue, ^{
NSLog(@"firstQueue");
});
dispatch_async(secondQueue, ^{
NSLog(@"secondQueue1");
});
dispatch_async(secondQueue, ^{
NSLog(@"secondQueue2");
});
/* 打印结果
firstQueue
secondQueue1
secondQueue2
*/
dispatch_after
作用:在指定时间后追加任务(不是执行任务)到 Dispatch Queue。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"do work");
});
Dispatch Group
作用:有时候希望在一个队列中的任务全部执行完毕后,获得结束通知,就可以采用 Dispatch Group。
使用 dispatch_group_notify 获取结束的通知
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
// 追加 Block 到指定的 queue 中,Block 持有 group 的引用。
dispatch_group_async(group, queue, ^{
NSLog(@"任务一");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务二");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务三");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"3个任务都结束后,在主线程执行相关操作");
});
使用 dispatch_group_wait 单纯等待全部任务的结束情况,没有结束通知的获取
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
// 追加 Block 到指定的 queue 中,Block 持有 group 的引用。
dispatch_group_async(group, queue, ^{
NSLog(@"任务一");
[NSThread sleepForTimeInterval:2];
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务二");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务三");
});
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
/* dispatch_group_wait 仅等待全部处理执行结束。
第二个参数是 dispatch_time_t 类型, 用来标识在规定时间内 group 中的任务是否全部完成。
完成返回 0,超时返回 1。
如要一直等待任务完成,第二个参数可以传入 DISPATCH_TIME_FOREVER,此时返回值恒为 0。
如果第二个参数传入 DISPATCH_TIME_NOW,则返回值恒为 1。
*/
long result = dispatch_group_wait(group, DISPATCH_TIME_NOW);
if (result == 0) {
NSLog(@"任务在规定时间内全部处理完成");
} else {
NSLog(@"任务超时");
}
ps:在一般情况下,推荐用 dispatch_group_notify 函数追加结束处理到所需的队列(一般是主队列)中。
dispatch_barrier_async
作用:队列中的任务执行到一半时,进行拦截,执行需要的操作,然后继续执行剩下的任务。
dispatch_queue_t queue = dispatch_queue_create("com.lincoln.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"读取1");
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"读取2");
});
dispatch_async(queue, ^{
NSLog(@"读取3");
});
// 当执行完上面队列中的任务后,进行拦截,接着再执行下面剩余的任务。
dispatch_barrier_async(queue, ^{
NSLog(@"进行数据写入");
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"读取4");
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"读取5");
});
dispatch_async(queue, ^{
NSLog(@"读取6");
});
dispatch_async(queue, ^{
NSLog(@"读取7");
});
dispatch_sync
作用:同步执行任务。这很容易造成死锁,需要谨慎。现举 2 个 死锁的例子。
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
NSLog(@"Hello?");
});
/* 上面的代码会造成死锁!!!原因如下:
现在主队列上有2个任务:
任务1:执行 dispatch_sync 函数
任务2:将 Block 添加到主队列
因为是同步,所以先执行任务 1,再执行任务 2。dispatch_sync 函数执行完毕的标志是成功将 Block 添加到了主队列。也就是说,任务1 执行的任务和任务 2 是同一个任务。那么这就造成了相互等待的问题,也就是死锁的问题。
*/
dispatch_queue_t serialQueue = dispatch_queue_create("com.lincoln.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"Hello?");
});
});
/*
这个和上面这个类似。上面这个是在主线程发生死锁,这个是在串行队列新开的线程中发生死锁。
*/
dispatch_apply
作用:按指定的次数将指定的 Block 追加到指定的队列中。(会阻塞线程)
// dispatch_apply 函数会等待任务全部执行完毕。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"index: %zu", index);
});
NSLog(@"done");
// 由于 dispatch_apply 函数也与 dispatch_sync 函数相同,会等待处理执行结束,因此推荐在 dispatch_async 中非同步的执行 dispatch_apply 函数。
NSArray *array = @[@"a", @"b", @"c"];
dispatch_async(queue, ^{
dispatch_apply(array.count, queue, ^(size_t index) {
NSLog(@"%zu: %@", index, array[index]);
});
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"返回主队列");
});
});
dispatch_suspend / dispatch_resume
作用:挂起和恢复队列。需要注意的是,正在队列中执行的任务是不受影响的。
Dispatch Semaphore
Dispatch Semaphore 是持有计数的信号,该计数是多线程中的技术类型信号。当计数值大于等于 1 时,可以执行排他操作,然后将计数减 1,当计数等于 0 时,不能执行排他操作。这是一种比 Dispatch Serial 和 dispatch_barrier_async 更细粒度的控制。
-
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
创建了一个信号。 -
long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
等待信号的计数大于等于1,第二个参数是等待时间,如果超时或者计数等于 0,则返回值 result 等于 1。如果计数值大于等于1,并且未超时,则返回值 result 等于 0,执行排他处理,并且计数值减 1。 -
dispatch_semaphore_signal(semaphore);
使计数值加 1。
下面函数可能很可能运行报错
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [NSMutableArray new];
for (int i = 0; i < 10000; i++) {
// 很可能导致多个线程同时访问 array
dispatch_async(queue, ^{
[array addObject:@(i)];
});
}
通过 Dispatch Semaphore 解决
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [NSMutableArray new];
// Dispatch Semaphore 初始化计数值为 1。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
// 一直等待,直到 Dispatch Semaphore 计数值大于等于 1。保证只有 1 条线程可访问 array 对象。
long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (result == 0) {
// 将 Dispatch Semaphore 计数值减 1,然后操作 array
[array addObject:@(i)];
}
// 将 Dispatch Semaphore 计数值加 1。
dispatch_semaphore_signal(semaphore);
});
}
ps:一般 Dispatch Semaphore 是在子线程环境下使用。
dispatch_once
作用:只执行一次,多用于创建单例。
- (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static Person *p;
dispatch_once(&onceToken, ^{
p = [[Person alloc] init];
});
return p;
}
Dispatch I/O
作用:对数据进行读写操作。可以一次使用多个线程并列地将一个较大的文件分割为一块一块地进行读取处理。分割读取的数据通过使用 Dispatch Data 可更为简单地进行结合和分割。
ps:因为对 Dispatch I/O 相关的 API 不太了解,故此处不提供 demo,读者可以上网查找。
GCD 的实现
Dispatch Queue
GCD 的实现需要在系统级实现,需要调用 XNU 系统内核,所以开发者编写管理线程的代码,在性能方面不可能胜过 XNU 内核级所实现的 GCD。
用于实现 Dispatch Queue 而使用的软件组件
组件名称 | 提供技术 |
---|---|
libdispatch | Dispatch Queue |
Libc(pthreads) | pthread_workqueue |
XNU 内核 | workqueue |
开发者所使用的 GCD 的API 都包含在 libdispatch 库中,这些 API 都是 C 语言函数。Dispatch Queue 通过结构体和链表,被实现为 FIFO 队列。
添加到 Dispatch Queue 中的 Block 并不直接加入 FIFO 队列,而是先加入 Dispatch Continuation 这一 dispatch_continuation_t 类型结构体中,然后再加入 FIFO 队列。该 Dispatch Continuation 用于记忆 Block 所属的 Dispatch Group 和其他一些信息,相当于一般常说的执行上下文。
Global Dispatch Queue 有 8 中优先级,分别为:
- High Priority
- Default Priority
- Low Priority
- Background Priority
- High Overcommit Priority
- Default Overcommit Priority
- Low Overcommit Priority
- Background Overcommit Priority
优先级中有 Overcommit 的 Global Dispatch Queue 使用在 Serial Dispatch Queue 中。带有 Overcommit 的优先级,不管系统状态如何,都会强制生成线程的 Dispatch Queue。
这 8 种 Global Dispatch Queue 各使用 1 个 pthread_workqueue。GCD 初始化时,使用 pthread_workqueue_create_np 函数生成 pthread_workqueue。
XNU 内核持有 4 种 workqueue,分别为:
- WORKQUEUE_HIGH_PRIOQUEUE
- WORKQUEUE_DEFAULT_PRIOQUEUE
- WORKQUEUE_LOW_PRIOQUEUE
- WORKQUEUE_BG_PRIOQUEUE
以上 4 中优先级的 workqueue 与 Global Dispatch Queue 的 4 种优先级相同。
Dispatch Queue 中执行 Block 的过程
- 当在 Global Dispatch Queue 中执行 Block 时,libdispatch 从 Global Dispatch Queue 自身的 FIFO 队列中取出 Dispatch Continuation,调用 pthread_workqueue_additem_np 函数。
- 将 Global Dispatch Queue 自身、符合其优先级的 workqueue 信息以及为执行 Dispatch Continuation 的回调函数等传递给参数。
- pthread_workqueue_additem_np 函数使用 workq_kernreturn 系统调用,通知 workqueue 增加应当执行的项目。
- 根据该通知,XNU 内核基于系统状态判断是否要生成线程。如果是 Overcommit 优先级的 Global Dispatch Queue,workqueue 则始终生成线程。
- workqueue 的线程执行 pthread_workqueue 函数,该函数调用 libdispatch 的回调函数。
- 在该回调函数中执行加入到 Dispatch Continuation 的 Block。
- Block 执行结束后,进行通知 Dispatch Group 结束、释放 Dispatch Continuation 等处理。
- 开始准备执行加入到 Global Dispatch Queue 中的下一个 Block。
Dispatch Source
Dispatch Source 是 BSD 系内核惯有功能 kqueue 的包装。kqueue 是在 XNU 内核中发生各种事件时,对各种事件进行处理的技术。其 CPU 负载非常小,尽量不占用资源。
kqueue 可以说是处理 XNU 内核中各种事件的方法中最优秀的一种。
Dispatch Source 的种类
名称 | 内容 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 变量增加,属于自定义事件 |
DISPATCH_SOURCE_TYPE_DATA_OR | 属于自定义事件,用法同上面的类型一样 |
DISPATCH_SOURCE_TYPE_MACH_SEND | Mach端口发送事件 |
DISPATCH_SOURCE_TYPE_MACH_RECV | Mach端口接收事件 |
DISPATCH_SOURCE_TYPE_PROC | 检测到与进程相关的事件 |
DISPATCH_SOURCE_TYPE_READ | 读取文件事件 |
DISPATCH_SOURCE_TYPE_WRITE | 写入文件事件 |
DISPATCH_SOURCE_TYPE_VNODE | 文件属性更改事件 |
DISPATCH_SOURCE_TYPE_SIGNAL | 接收信号事件 |
DISPATCH_SOURCE_TYPE_TIMER | 定时器事件 |
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE | 内存压力事件 |
source_type_read
- (void)source_type_read {
__block size_t total = 0;
// 要读取的字节数
size_t size = 1024 * 1024;
char *buff = (char *)malloc(size);
NSString *filePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *fileName = [filePath stringByAppendingPathComponent:@"markdown语法.md"];
int fd = open([fileName UTF8String], O_WRONLY | O_CREAT | O_TRUNC, (S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
NSLog(@"write fd: %d", fd);
if (fd == -1) return;
// 设定为异步映像
fcntl(fd, F_SETFL, O_NONBLOCK);
// 获取用于追加事件处理的 Global Dispatch Queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 基于 READ 事件的 Dispatch Source
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
// 指定发生 READ 事件时执行的处理
dispatch_source_set_event_handler(source, ^{
// 获取可读写的字节数
size_t available = dispatch_source_get_data(source);
// 从映像中读取
ssize_t length = read(fd, buff, available);
// 发生错误时取消 Dispatch Source
if (length < 0) {
dispatch_source_cancel(source);
}
total += length;
if (total == size) {
// buff 的处理
NSLog(@"对 buff 进行处理");
// 处理结束后,取消 Dispatch Source
dispatch_source_cancel(source);
}
});
// 指定取消 Dispatch Source 时的处理
dispatch_source_set_cancel_handler(source, ^{
free(buff);
close(fd);
});
// 启动 Dispatch Source
dispatch_resume(source);
}
source_type_timer
- (void)source_type_timer {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 将定时器设置为 15 秒后,不重复,允许延迟 1 秒。
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 15ull * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 1ull * NSEC_PER_SEC);
// 指定定时器指定时间内执行的处理
dispatch_source_set_event_handler(timer, ^{
NSLog(@"wake up!");
// 取消 Dispatch Source
dispatch_source_cancel(timer);
});
//指定取消 Dispatch Source 时的处理
dispatch_source_set_cancel_handler(timer, ^{
NSLog(@"cancelled");
});
// 启动 Dispatch Source
dispatch_resume(timer);
}
参考
Dispatch Source学习
细说GCD
Coucurrency Programming Guide
demo 地址:Github
网友评论