Grand Central Dispatch
是这本书的最后一章,作者先从 CPU 多核命令执行简述多线程编程的概念,然后开始逐个分析 GCD 中常用的 API,到最后底层分析 GCD 的源码实现。这里参考 《Objective-C 高级编程》干货三部曲(三):GCD篇,来对这最后一个篇章的内容作总结学习。
写在前面:
书中描述的,开发者要自己使用函数 dispatch_retain
、dispatch_release
手动管理队列的内存,是在低于 iOS 6.0 or Mac OS X 10.8 以下的系统中需要做的事情。在 iOS 6.0 or Mac OS X 10.8 及更高版本中,GCD 对象已经纳入 ARC 的管理,所以这两个函数也不再用了。感兴趣可搜索宏 OS_OBJECT_USE_OBJC
查看更多相关信息。
多线程编程
多线程编程实际上是一种易发生各种问题的编程技术:
- 数据竞争:多个线程更新相同的资源会导致信息不一致。
- 死锁:多个线程相互持续等待。
- 太多线程会消耗大量内存。
虽然使用多线程编程会使程序复杂化,但是仍然是必须的:因为使用多线程编程可保证应用程序的响应性能。
如果耗时操作阻塞了主线程的 RunLoop,会导致用户界面不能更新、应用程序的画面长时间停滞等问题,所以需要将耗时操作放到子线程中处理。
队列 Dispatch Queue
苹果官方文档对 GCD 的说明:开发者要做的只是定义想要执行的任务并追加到适当的 Dispatch Queue 中。
Dispatch Queue 即是执行处理的等待队列,其按照先进先出 FIFO 的顺序执行添加的任务。在执行处理时存在以下两种 Dispatch Queue:
- Serial Dispatch Queue,串行队列,即等待现在正在执行的任务处理结束,然后在处理下一个。
- Concurrent Dispatch Queue,并发队列,即不等待现在正在执行的任务处理结束,并开始下一个任务。
创建队列 dispatch_queue_create
通过 dispatch_queue_create
函数可以创建队列,第一个参数是队列名,第二参数,是 NULL
和 DISPATCH_QUEUE_SERIAL
时,返回串行队列,如果是 DISPATCH_QUEUE_CONCURRENT
,则返回并发队列。
还可以自己通过函数 dispatch_queue_attr_make_with_
来自定义队列参数,不过很少使用。
另外,在低于 iOS 6.0 or Mac OS X 10.8 以下的系统中,需要开发者自己通过 dispatch_retain
、dispatch_release
函数管理 GCD 的内存。在 iOS 6.0 or Mac OS X 10.8 及更高版本中,GCD 对象已经纳入 ARC 的管理,所以这两个函数也不再用了
串行队列 Serial Dispatch Queue
将任务追加到穿行队列:
/// 串行队列
-(void)serialQueue {
dispatch_queue_t serialQ = dispatch_queue_create("serial queue", NULL);
for (int i = 0; i < 6; i++) {
dispatch_async(serialQ, ^{
NSLog(@"task index in serial queue: %d thread: %@", i, [NSThread currentThread]);
});
}
}
输出:
2019-12-12 17:26:33.748110+0800 GCD[25621:1562739] task index in serial queue: 0 thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748335+0800 GCD[25621:1562739] task index in serial queue: 1 thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748498+0800 GCD[25621:1562739] task index in serial queue: 2 thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748642+0800 GCD[25621:1562739] task index in serial queue: 3 thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748775+0800 GCD[25621:1562739] task index in serial queue: 4 thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
2019-12-12 17:26:33.748935+0800 GCD[25621:1562739] task index in serial queue: 5 thread: <NSThread: 0x600000c20940>{number = 5, name = (null)}
对于串行队列,都是以阻塞的形式顺序执行的,这里只是做个验证。
一旦生成 Serial Dispatch Queue 并追加处理,系统对于一个 Serial Dispatch Queue 就只生成并使用一个线程。如果生成2000个 Serial Dispatch Queue 并追加处理,那么就生成2000个线程。可是过多线程会消耗大量内存,引起大量的上下文切换,大幅度降低系统响应性能 ,所以我们应该只在必要时考虑使用 Serial Dispatch Queue,譬如多个线程更新相同资源导致数据竞争时。
如果我们将6个任务追加到6个 Serial Dispatch Queue 中,那么系统就会同时处理这6个任务,创建6个子线程:
/// 串行队列
-(void)serialQueue {
for (int i = 0; i < 6; i++) {
dispatch_queue_t serialQ = dispatch_queue_create("serial queue", NULL);
dispatch_async(serialQ, ^{
NSLog(@"task index in serial queue: %d thread: %@", i, [NSThread currentThread]);
});
}
}
输出:
2019-12-12 17:45:50.996286+0800 GCD[26487:1627877] task index in serial queue: 4 thread: <NSThread: 0x600002cf7980>{number = 7, name = (null)}
2019-12-12 17:45:50.996286+0800 GCD[26487:1627881] task index in serial queue: 1 thread: <NSThread: 0x600002cca7c0>{number = 6, name = (null)}
2019-12-12 17:45:50.996286+0800 GCD[26487:1627884] task index in serial queue: 5 thread: <NSThread: 0x600002ce1500>{number = 5, name = (null)}
2019-12-12 17:45:50.996332+0800 GCD[26487:1627878] task index in serial queue: 3 thread: <NSThread: 0x600002cfb080>{number = 3, name = (null)}
2019-12-12 17:45:50.996343+0800 GCD[26487:1627894] task index in serial queue: 2 thread: <NSThread: 0x600002cf7a40>{number = 8, name = (null)}
2019-12-12 17:45:50.996363+0800 GCD[26487:1627880] task index in serial queue: 0 thread: <NSThread: 0x600002cf01c0>{number = 4, name = (null)}
从结果来看,这里的6个任务并不是顺序执行的。并且创建了6个线程来处理。
并发队列 Concurrent Dispatch Queue
iOS 和 MacOS 的核心--XNU 内核决定应当使用的线程数,并只生成所需要的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XNU 内核会结束不再需要的线程。XNU 内核仅使用 Concurrent Dispatch Queue 便可完美地管理并执行多个处理的线程。
下面将任务追加到并发队列:
/// 并发队列
-(void)serialQueue {
dispatch_queue_t concurrentQ = dispatch_queue_create("sconcurrent queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 6; i++) {
dispatch_async(concurrentQ, ^{
NSLog(@"task index in serial queue: %d thread: %@", i, [NSThread currentThread]);
});
}
}
输出:
2019-12-12 17:44:01.782371+0800 GCD[26399:1622122] task index in serial queue: 0 thread: <NSThread: 0x60000070bdc0>{number = 5, name = (null)}
2019-12-12 17:44:01.782437+0800 GCD[26399:1622119] task index in serial queue: 1 thread: <NSThread: 0x600000758d40>{number = 4, name = (null)}
2019-12-12 17:44:01.782575+0800 GCD[26399:1622119] task index in serial queue: 4 thread: <NSThread: 0x600000758d40>{number = 4, name = (null)}
2019-12-12 17:44:01.782579+0800 GCD[26399:1622122] task index in serial queue: 5 thread: <NSThread: 0x60000070bdc0>{number = 5, name = (null)}
2019-12-12 17:44:01.782623+0800 GCD[26399:1622117] task index in serial queue: 3 thread: <NSThread: 0x600000764a80>{number = 6, name = (null)}
2019-12-12 17:44:01.782609+0800 GCD[26399:1622118] task index in serial queue: 2 thread: <NSThread: 0x600000741840>{number = 3, name = (null)}
系统提供的队列
Main Dispatch Queue
在主线程中执行的 Dispatch Queue。因为主线程只有一个,所以 Main Dispatch Queue 自然就是 Serial Dispatch Queue。追加到 Main Dispatch Queue 的任务在主线程的 RunLoop 中执行。
获取方法:
// Main Dispatch Queue 的获取方法
dispatch_queue_t mianQueue = dispatch_get_main_queue();
Global Dispatch Queue
Global Dispatch Queue 是所有引用程序都能够使用的 Concurrent Dispatch Queue。我们没有必要通过 dispatch_queue_create 函数逐个生成 Concurrent Dispatch Queue,只要获取 Global Dispatch Queue 使用即可。
Global Dispatch Queue 有四个优先等级:
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
- DISPATCH_QUEUE_PRIORITY_BACKGROUND
获取方式如下:
// Global Dispatch Queue 高优先级 获取方法
dispatch_queue_t highGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// Global Dispatch Queue 默认优先级 获取方法
dispatch_queue_t defaultGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// Global Dispatch Queue 低优先级 获取方法
dispatch_queue_t lowGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
// Global Dispatch Queue 后台优先级 获取方法
dispatch_queue_t backgroundGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
其中第二个参数 flag 默认为0,官方解释为 Reserved for future use. Passing any value other than zero may result in a NULL return value.
。为未来预留的字段,如果不设为 0, 就可能返回一个空的队列。
举个简单的例子来说明二者的使用场景:
//获取全局并发队列进行耗时操作
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//加载图片
NSData *dataFromURL = [NSData dataWithContentsOfURL:imageURL];
UIImage *imageFromData = [UIImage imageWithData:dataFromURL];
dispatch_async(dispatch_get_main_queue(), ^{
//获取主队列,在图片加载完成后更新UIImageView
UIImageView *imageView = [[UIImageView alloc] initWithImage:imageFromData];
});
});
GCD 的 API 们
dispatch_set_target_queue
这个函数有两个作用:
- 改变队列的优先级
- 阻止多个串行队列的并发执行。注意,是 多个 串行队列,不是一个串行队列下的多个任务。
改变队列的优先级
首先要知道一点,dispatch_queue_create
函数生成的 Dispatch Queue 不论是 Serial Dispatch Queue,还是 Concurrent Dispatch Queue,都使用和 Global Dispatch Queue 默认优先级相同等级的线程。即 DISPATCH_QUEUE_PRIORITY_DEFAULT。
如果想要改变生成队列的优先级,就需要使用 dispatch_set_target_queue 函数,如下:
/// 改变队列优先级
-(void)changePriority {
dispatch_queue_t defaultQueue = dispatch_queue_create("serial queue", NULL);
dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
// 第一个参数:需要改变优先级的队列
// 第二个参数:目标队列,即作为参照物的队列。
dispatch_set_target_queue(defaultQueue, bgQueue);
// 此时,defaultQueue 的执行优先级为 DISPATCH_QUEUE_PRIORITY_BACKGROUND
}
注意,第一个参数必须是通过 dispatch_queue_create
函数生成的 Dispatch Queue,如果使用系统提供的 Main Dispatch Queue 和 Global Dispatch Queue 则不知道会出现什么状况,因为系统提供的队列不可更改优先级。
阻止多个串行队列的并发执行
有时我们希望多个 Serial Dispatch Queue 可以顺序执行,便可以通过 dispatch_set_target_queue 函数来做执行约束。如下:
/// 多个 Serial Dispatch Queue 并发执行
-(void)someSerialQueue {
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
// 创建 5 个串行队列
dispatch_queue_t serialQ = dispatch_queue_create("serial queue", NULL);
[array addObject:serialQ];
}
// 并发执行
[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_async(queue, ^{
NSLog(@"任务ID: %ld", idx);
});
}];
}
输出:
2019-12-11 15:21:02.999308+0800 GCD[57283:11864893] 任务ID: 2
2019-12-11 15:21:02.999310+0800 GCD[57283:11864896] 任务ID: 4
2019-12-11 15:21:02.999353+0800 GCD[57283:11864889] 任务ID: 1
2019-12-11 15:21:02.999379+0800 GCD[57283:11864892] 任务ID: 3
2019-12-11 15:21:02.999497+0800 GCD[57283:11864891] 任务ID: 0
可以看到,当我使用 dispatch_async 异步执行这些任务时,他们执行顺序是不固定的。
通过 dispatch_set_target_queue 可以阻止这种并发现象:
-(void)someSerialQueue {
NSMutableArray *array = [NSMutableArray array];
dispatch_queue_t targetQueue = dispatch_queue_create("target queue", NULL);
for (int i = 0; i < 5; i++) {
// 创建 5 个串行队列
dispatch_queue_t serialQ = dispatch_queue_create("serial queue", NULL);
dispatch_set_target_queue(serialQ, targetQueue);
[array addObject:serialQ];
}
// 并发执行
[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_async(queue, ^{
NSLog(@"任务ID: %ld", idx);
});
}];
}
输出:
2019-12-11 15:27:38.262131+0800 GCD[57724:11885714] 任务ID: 0
2019-12-11 15:27:38.262377+0800 GCD[57724:11885714] 任务ID: 1
2019-12-11 15:27:38.262551+0800 GCD[57724:11885714] 任务ID: 2
2019-12-11 15:27:38.262690+0800 GCD[57724:11885714] 任务ID: 3
2019-12-11 15:27:38.262838+0800 GCD[57724:11885714] 任务ID: 4
其实,即使不使用 dispatch_set_target_queue
,而是将 dispatch_async
修改为 dispatch_sync
或者直接去掉 dispatch_async
,直接 NSLog
输出,都可以顺序打印,这里只是举这么例子证明这个函数有此功能。
dispatch_after
dispatch_after 解决的问题:在某个线程,在指定时间段后,执行某个任务。如下:
-(void)dispatchAfter {
dispatch_after(DISPATCH_TIME_NOW + 3 * NSEC_PER_SEC, dispatch_get_main_queue(), ^{
NSLog(@"3 秒后将 NSLog 事件追加到主线程中");
});
}
注意: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_group
如果遇到一个需求:在全部任务处理完毕后,执行某个操作。如果只使用一个 Serial Dispatch Queue,只需要将想要执行的处理全部追加到后面即可。但是实际开发中,这种方式很容易造成线程的阻塞,所以大都还是需要使用 dispatch_group 来实现的。譬如某个页面有3个接口数据组成,拿到三个接口数据后刷新界面这样的简单需求。
-(void)dispatchGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 5; i++) {
dispatch_group_async(group, queue, ^{
NSLog(@"任务ID: %d", i);
});
}
dispatch_group_notify(group, queue, ^{
NSLog(@"任务执行完毕了");
});
}
输出:
2019-12-11 15:59:22.567149+0800 GCD[59633:11996591] 任务ID: 0
2019-12-11 15:59:22.567161+0800 GCD[59633:11996593] 任务ID: 2
2019-12-11 15:59:22.567149+0800 GCD[59633:11996594] 任务ID: 1
2019-12-11 15:59:22.567277+0800 GCD[59633:11996598] 任务ID: 3
2019-12-11 15:59:22.567406+0800 GCD[59633:11996593] 任务ID: 4
2019-12-11 15:59:22.567543+0800 GCD[59633:11996593] 任务执行完毕了
dispatch_group_notify
函数的第一个参数是要监听的 Dispatch Group。在追加到该 Group 的全部任务处理执行结束时,将第三个参数的 Block 追加到第二个参数的 Dispatch Queue 中。
有时候我们根据具体需求,也可以使用 dispatch_group_enter
和 dispatch_group_leave
两个函数来处理组任务的需求:
-(void)dispatchGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 第 1 个网络请求
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"第 1 个请求结束");
dispatch_group_leave(group);
});
// 第 2 个网络请求
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"第 2 个请求结束");
dispatch_group_leave(group);
});
// 第 3 个网络请求
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"第 3 个请求结束");
dispatch_group_leave(group);
});
dispatch_group_notify(group, queue, ^{
NSLog(@"任务执行完毕了");
});
}
输出:
2019-12-11 16:16:20.065807+0800 GCD[60651:12053195] 第 1 个请求结束
2019-12-11 16:16:20.065956+0800 GCD[60651:12053192] 第 3 个请求结束
2019-12-11 16:16:20.066017+0800 GCD[60651:12053194] 第 2 个请求结束
2019-12-11 16:16:20.066156+0800 GCD[60651:12053194] 任务执行完毕了
dispatch_group_wait
dispatch_group_wait 配合 dispatch_group 使用,用来判断指定时间范围内,添加的组任务是否全部完成。可根据返回的结果,作区分处理。一共有两种情况:
- 返回结果为 0 : 组任务完成没有超时。
- 返回结果不为 0 :组任务执行超时。
任务超时
-(void)dispatchGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 5; i++) {
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 1000000000; i++) {
}
NSLog(@"任务ID: %d", i);
});
}
long result1 = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (result1 == 0) {
NSLog(@"result1: group 内部的任务全部结束");
} else {
NSLog(@"result1: 在指定时间内,group 内部任务没有全部完成。 result1 = %ld", result1);
}
}
输出:
2019-12-11 16:35:43.881997+0800 GCD[61895:12119734] result1: 在指定时间内,group 内部任务没有全部完成。 result1 = 49
2019-12-11 16:35:47.280371+0800 GCD[61895:12119867] 任务ID: 2
2019-12-11 16:35:47.338385+0800 GCD[61895:12119863] 任务ID: 0
2019-12-11 16:35:47.346969+0800 GCD[61895:12119865] 任务ID: 1
2019-12-11 16:35:47.356539+0800 GCD[61895:12119864] 任务ID: 3
2019-12-11 16:35:50.670539+0800 GCD[61895:12119867] 任务ID: 4
任务未超时:
-(void)dispatchGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 5; i++) {
dispatch_group_async(group, queue, ^{
NSLog(@"任务ID: %d", i);
});
}
long result1 = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (result1 == 0) {
NSLog(@"result1: group 内部的任务全部结束");
} else {
NSLog(@"result1: 在指定时间内,group 内部任务没有全部完成。 result1 = %ld", result1);
}
}
输出:
2019-12-11 16:37:04.044337+0800 GCD[61979:12123344] 任务ID: 1
2019-12-11 16:37:04.044345+0800 GCD[61979:12123342] 任务ID: 2
2019-12-11 16:37:04.044383+0800 GCD[61979:12123345] 任务ID: 3
2019-12-11 16:37:04.044374+0800 GCD[61979:12123343] 任务ID: 0
2019-12-11 16:37:04.044542+0800 GCD[61979:12123344] 任务ID: 4
2019-12-11 16:37:04.044672+0800 GCD[61979:12123199] result1: group 内部的任务全部结束
如果将时间设置为 DISPATCH_TIME_FOREVER
,则进入不设限制时间的等待,直到组任务全部完成,dispatch_group_wait 函数返回结果为 0。如果时间设置为 DISPATCH_TIME_NOW
,即为不等待,直接判断返回结果。
虽然利用 RunLoop 循环可以检测任务是否结束,而不消耗多余等待时间,但是书籍作者还是更推荐使用 dispatch_group_notify,可以简化代码。
dispatch_barrier_async
dispatch_barrier_async 又称为栅栏函数,简单来说,并发队列的多个任务,执行顺序是不固定的,但是如果中间插入了 dispatch_barrier_async 函数,就会以此函数为界限,分别执行。举例如下:
-(void)dispatchBarrier {
dispatch_queue_t queue = dispatch_queue_create("concurrent queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"第 1 次 读取数据");
});
dispatch_async(queue, ^{
NSLog(@"第 2 次 读取数据");
});
dispatch_async(queue, ^{
NSLog(@"第 3 次 读取数据");
});
dispatch_barrier_async(queue, ^{
NSLog(@"这条数据被修改了,此后需要使用新数据");
})
dispatch_async(queue, ^{
NSLog(@"第 1 次 获取最新数据");
});
dispatch_async(queue, ^{
NSLog(@"第 2 次 获取最新数据");
});
dispatch_async(queue, ^{
NSLog(@"第 3 次 获取最新数据");
});
}
输出:
2019-12-12 10:21:26.499064+0800 GCD[6242:86373] 第 2 次 读取数据
2019-12-12 10:21:26.499064+0800 GCD[6242:86376] 第 1 次 读取数据
2019-12-12 10:21:26.499099+0800 GCD[6242:86375] 第 3 次 读取数据
2019-12-12 10:21:26.499271+0800 GCD[6242:86376] 这条数据被修改了,此后需要使用新数据
2019-12-12 10:21:26.499412+0800 GCD[6242:86376] 第 1 次 获取最新数据
2019-12-12 10:21:26.499418+0800 GCD[6242:86373] 第 3 次 获取最新数据
2019-12-12 10:21:26.499424+0800 GCD[6242:86375] 第 2 次 获取最新数据
dispatch_sync
目前所有的例子都使用的异步函数,相对的,也有 同步
函数。这里简单区分下:
- dispatch_async:异步函数,这个函数时立即返回。就是将指定的 Block 非同步 的追加到指定的 Dispatch Queue 中。dispatch_async 不做任何等待。
- dispatch_sync:同步函数,这个函数不会立即返回。就是将指定的 Block 同步 的追加到指定的 Dispatch Queue 中。再追加的 Block 结束前,dispatch_async 函数会一直等待。
下面举个例子说明:
-(void)dispatchSync {
// 同步处理
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"开始同步处理");
__block NSInteger num = 0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
// 模仿耗时操作
for (NSInteger i = 0; i< 1000000000; i++) {
num ++;
}
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"同步处理结束");
});
NSLog(@"num: %ld", num);
NSLog(@"当前线程:%@", [NSThread currentThread]);
}
输出:
2019-12-12 10:34:57.951023+0800 GCD[6908:133503] 当前线程:<NSThread: 0x6000012a8180>{number = 1, name = main}
2019-12-12 10:34:57.951307+0800 GCD[6908:133503] 开始同步处理
2019-12-12 10:35:00.762171+0800 GCD[6908:133503] 当前线程:<NSThread: 0x6000012a8180>{number = 1, name = main}
2019-12-12 10:35:00.762339+0800 GCD[6908:133503] 同步处理结束
2019-12-12 10:35:00.762436+0800 GCD[6908:133503] num: 1000000000
2019-12-12 10:35:00.762595+0800 GCD[6908:133503] 当前线程:<NSThread: 0x6000012a8180>{number = 1, name = main}
通过输出的时间可以看到,即使 dispatch_sync 函数内部执行了 3 秒钟,依然要等待执行完,才会继续执行下面的代码。即以阻塞线程的执行。并且始终在主线程。
下面用异步函数执行相同的代码逻辑:
-(void)dispatchAsync {
// 同步处理
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"开始异步处理");
__block NSInteger num = 0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// 模仿耗时操作
for (NSInteger i = 0; i< 1000000000; i++) {
num ++;
}
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"异步处理结束");
});
NSLog(@"num: %ld", num);
NSLog(@"当前线程:%@", [NSThread currentThread]);
}
输出:
2019-12-12 10:40:06.457260+0800 GCD[7190:149132] 当前线程:<NSThread: 0x600003e0dc40>{number = 1, name = main}
2019-12-12 10:40:06.457464+0800 GCD[7190:149132] 开始异步处理
2019-12-12 10:40:06.457614+0800 GCD[7190:149132] num: 0
2019-12-12 10:40:06.457854+0800 GCD[7190:149132] 当前线程:<NSThread: 0x600003e0dc40>{number = 1, name = main}
2019-12-12 10:40:09.495490+0800 GCD[7190:149279] 当前线程:<NSThread: 0x600003e50040>{number = 4, name = (null)}
2019-12-12 10:40:09.497207+0800 GCD[7190:149279] 异步处理结束
可以看到 异步函数 dispatch_async 内部其实已经不在主线程执行了。也未阻塞主线程的执行。
另外,即使将 dispatch_async 后的 Block 追加到串行队列中,依然不会阻塞主线程,因为这是异步函数 dispatch_async 的功能, 和所操作的队列无关。
另外,同步函数非常容易造成死锁,如下:
-(void)deadLock {
NSLog(@"任务1");
dispatch_queue_t mainQ = dispatch_get_main_queue();
dispatch_sync(mainQ, ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
}
上面代码运行时会引发崩溃。该源代码在 Main Dispatch Queue 即主线程中执行追加的 Block,并等待其执行结束。而其实主线程正在执行这些代码,所以无法执行追加到 Main Dispatch Queue 的 Block。
简单来说,因为任务2 被追加到主队列最后,它需要等到任务3 执行结束,但又是同步函数,所以任务3也在等待任务2执行完成。二者相互等待,形成死锁。
对于下面这段代码:
-(void)deadLock {
NSLog(@"任务1");
dispatch_queue_t mainQ = dispatch_get_main_queue();
dispatch_async(mainQ, ^{
NSLog(@"任务2");
dispatch_sync(mainQ, ^{
NSLog(@"任务3");
});
NSLog(@"任务4");
});
NSLog(@"任务5");
}
输出:
2019-12-12 11:10:09.232553+0800 GCD[8612:258269] 任务1
2019-12-12 11:10:09.233120+0800 GCD[8612:258269] 任务5
2019-12-12 11:10:09.278799+0800 GCD[8612:258269] 任务2
执行后依然会运行时崩溃,这里也形成了死锁。dispatch_async 函数追加的 Block 内部形成死锁。
当我们使用同步函数 dispatch_sync 时,一定要多加注意,否则非常容易引发问题。
dispatch_apply
dispatch_apply 函数是 dispatch_sync 函数和 Dispatch Group 的关联 API。该函数按照指定的次数将指定的 Block 追加到指定的 Dispatch Queue 中,并等待全部处理执行结束:
-(void)dispatchApply {
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(@"apply 执行完毕");
}
输出:
2019-12-12 11:19:16.722810+0800 GCD[9034:290586] index: 1
2019-12-12 11:19:16.722839+0800 GCD[9034:290589] index: 2
2019-12-12 11:19:16.722863+0800 GCD[9034:290584] index: 3
2019-12-12 11:19:16.723002+0800 GCD[9034:290589] index: 5
2019-12-12 11:19:16.723002+0800 GCD[9034:290586] index: 4
2019-12-12 11:19:16.723009+0800 GCD[9034:290584] index: 6
2019-12-12 11:19:16.723104+0800 GCD[9034:290589] index: 7
2019-12-12 11:19:16.723151+0800 GCD[9034:290586] index: 8
2019-12-12 11:19:16.723204+0800 GCD[9034:290584] index: 9
2019-12-12 11:19:16.722801+0800 GCD[9034:290372] index: 0
2019-12-12 11:19:16.726093+0800 GCD[9034:290372] apply 执行完毕
我们也可以用这个函数来遍历数组:
-(void)dispatchApply {
NSArray *array = @[@1,@10,@43,@13,@33];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%@",array[index]);
});
NSLog(@"数组遍历完毕");
}
输出:
2019-12-12 11:23:15.304275+0800 GCD[9223:304025] 1
2019-12-12 11:23:15.304278+0800 GCD[9223:304185] 10
2019-12-12 11:23:15.304346+0800 GCD[9223:304184] 43
2019-12-12 11:23:15.304367+0800 GCD[9223:304183] 13
2019-12-12 11:23:15.304471+0800 GCD[9223:304185] 33
2019-12-12 11:23:15.304623+0800 GCD[9223:304025] 数组遍历完毕
可以看到 dispatch_apply 函数同 dispatch_sync 函数一样,都会阻塞线程,所以推荐在 dispatch_async 函数中非同步的执行 dispatch_apply 函数:
- (void)dispatch_apply_3
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSArray *array = @[@1,@10,@43,@13,@33];
__block NSInteger sum = 0;
dispatch_apply([array count], queue, ^(size_t index) {
NSNumber *number = array[index];
NSInteger num = [number integerValue];
sum += num;
});
dispatch_async(dispatch_get_main_queue(), ^{
//回到主线程,拿到总和
NSLog(@"完毕");
NSLog(@"%ld",sum);
});
});
}
dispatch_suspend/dispatch_resume
dispatch_suspend 函数挂起指定的 Dispatch Queue:
dispatch_suspend(queue);
dispatch_resume 函数恢复指定的 Dispatch Queue:
dispatch_resume(queue);
这些函数对已经执行的处理没有影响。挂起后,追加到 Dispatch Queue 中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。
Dispatch Semaphore
GCD 的信号量函数,相比 Serial Dispatch Queue 和 dispatch_barrier_async,信号量函数有着更细粒度的排他控制。
dispatch_semaphore_create
dispatch_semaphore_create 函数用来创建信号量:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
这是一个可失败的构造器,如果参数 value 的值小于等于0,则创建失败,对于参数 value 注释如下:
The starting value for the semaphore. Passing a value less than zero will cause NULL to be returned.
dispatch_semaphore_wait
dispatch_semaphore_wait 返回一个 long 值,Returns zero on success, or non-zero if the timeout occurred.
,返回 0 时,成功,非 0 则失败:
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
其中第二个参数与 dispatch_group_wait
函数的参数相同。
Dispatch Semaphore 示例
下面使用一个 for 循环中的异步函数来看下 Dispatch Semaphore 的特点:
-(void)dispatchSemaphore {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 信号量的初始值设置为 4
// 保证可访问 NSMUtableArray 类对象的线程同时只能有 1 个!
dispatch_semaphore_t semaphore = dispatch_semaphore_create(4);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
// 等待 Dispatch Semaphore
// 这里设置一直等待,直到 Dispatch Semaphore 的计数值达到大于等于 1
NSLog(@"信号量等待前 index = %d", i);
long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"信号量等待结束 result: %ld index = %d", result, i);
/*
* 由于 Dispatch Semaphore 的计数值达到大于等于 1
* 所以将 Dispatch Semaphore 的计数值减去 1
* dispatch_semaphore_wait 函数执行返回
*
* 执行到此时,Dispatch Semaphore 的计数值恒为 0。
*
* 由于可访问 NSMUtableArray 类对象的线程只有一个,因此可以安全的更新
*/
[array addObject:[NSNumber numberWithInt:i]];
/*
* 排他控制结束
* 所以通过函数 dispatch_semaphore_signal 函数将 Dispatch Semaphore 的计数值增加 1。
*
* 如果有通过 dispatch_semaphore_wait 函数等待 Dispatch Semaphore 的计数值 增加的线程,
* 就由最先等待的线程结束
*/
dispatch_semaphore_signal(semaphore);
});
}
NSLog(@"数组操作结束 array count = %ld", [array count]);
}
输出:
2019-12-12 14:24:27.972120+0800 GCD[17626:950806] 数组操作结束 array count = 0
2019-12-12 14:24:27.972120+0800 GCD[17626:950949] 信号量等待前 index = 0
2019-12-12 14:24:27.972120+0800 GCD[17626:950950] 信号量等待前 index = 1
2019-12-12 14:24:27.972157+0800 GCD[17626:950951] 信号量等待前 index = 2
2019-12-12 14:24:27.972181+0800 GCD[17626:950948] 信号量等待前 index = 3
2019-12-12 14:24:27.972349+0800 GCD[17626:950984] 信号量等待前 index = 5
2019-12-12 14:24:27.972266+0800 GCD[17626:950983] 信号量等待前 index = 4
2019-12-12 14:24:27.972364+0800 GCD[17626:950949] 信号量等待结束 result: 0 index = 0
2019-12-12 14:24:27.972443+0800 GCD[17626:950951] 信号量等待结束 result: 0 index = 2
2019-12-12 14:24:27.972449+0800 GCD[17626:950950] 信号量等待结束 result: 0 index = 1
2019-12-12 14:24:27.972464+0800 GCD[17626:950948] 信号量等待结束 result: 0 index = 3
2019-12-12 14:24:27.973015+0800 GCD[17626:950949] 信号量等待前 index = 6
2019-12-12 14:24:27.973104+0800 GCD[17626:950984] 信号量等待结束 result: 0 index = 5
2019-12-12 14:24:27.973202+0800 GCD[17626:950985] 信号量等待前 index = 7
2019-12-12 14:24:27.973626+0800 GCD[17626:950951] 信号量等待前 index = 8
2019-12-12 14:24:27.973750+0800 GCD[17626:950983] 信号量等待结束 result: 0 index = 4
2019-12-12 14:24:27.974067+0800 GCD[17626:950948] 信号量等待前 index = 9
2019-12-12 14:24:27.974094+0800 GCD[17626:950949] 信号量等待结束 result: 0 index = 6
2019-12-12 14:24:27.975372+0800 GCD[17626:950951] 信号量等待结束 result: 0 index = 8
2019-12-12 14:24:27.975698+0800 GCD[17626:950948] 信号量等待结束 result: 0 index = 9
2019-12-12 14:24:27.976428+0800 GCD[17626:950985] 信号量等待结束 result: 0 index = 7
这里针对 dispatch_semaphore_create 函数的参数简单说明下,它像是 信号量执行前的预等待次数,如果写1, 数组操作结束
输出打印较晚,如果数值较大,譬如 4, 数组操作结束
输出就会优先打印出来。
dispatch_once
dispatch_once 函数是保证应用程序执行中只执行一次指定处理的 API,而且在多线程环境下执行,也是线程安全的。主要用于单例模式。
- (void)onceCode
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"只执行一次的代码");
});
}
Dispatch I/O
Dispatch I/O 主要用于提高文件的读取速度。
总结
《Objective-C高级编程》这本书一共三章,书虽然买了几年了,但确实是很正经的完整读了第一遍,这里做个笔记,方便以后来看看吧。