笔记 - 多线程之GCD
2019-11-21 本文已影响0人
强子ly
目录
- 概念
- 常用API的使用
- 关于GCD的面试题
一、概念
- 1.1、什么是GCD?
定义想执行的任务,并追加到适当的 Dispatch Queue中
- 1.2、GCD的队列的概念
并发队列(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步(dispatch_async)函数下才有效
串行队列(Serial Dispatch Queue)
- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
- 1.3、容易混淆的术语 (同步、异步、并发、串行)
同步和异步主要影响:能不能开启新线程
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
并发和串行主要影响:任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
各种队列的执行效果.png
二、常用API的使用
- 2.1、dispatch_sync
dispatch_sync(<#dispatch_queue_t _Nonnull queue#>,
<#^(void)block#>)
用同步的方式执行任务
- 参数一、队列
- 参数二、任务
- 2.2、dispatch_async
dispatch_async(<#dispatch_queue_t _Nonnull queue#>,
<#^(void)block#>)
- 参数一、队列
- 参数二、任务
-
2.3、获取 Dispatch Queue
-
2.3.1、通过API的生成(dispatch_queue_create)
dispatch_queue_create(<#const char * _Nullable label#>,
<#dispatch_queue_attr_t _Nullable attr#>);
参数一:指定生成返回的Dispatch Queue的名称
- 推荐使用应用程序ID这种逆序全程域名,在Instruments及CrashLog中方便调试
参数二:指定生成返回的Dispatch Queue的类型
- 指定为 NULL 或 DISPATCH_QUEUE_SERIAL,生成Serial Dispatch Queue;
- 指定为DISPATCH_QUEUE_CONCURRENT,生成Concurrent Dispatch Queue
例:
dispatch_queue_t creatQueue1 = dispatch_queue_create("com.example.gcd.xxx",
NULL);
dispatch_queue_t creatQueue2 = dispatch_queue_create("com.example.gcd.xxx",
DISPATCH_QUEUE_SERIAL);
dispatch_queue_t creatQueue3 = dispatch_queue_create("com.example.gcd.xxx",
DISPATCH_QUEUE_CONCURRENT);
- 2.3.2、获取系统提供的Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
- 在主线程中执行的 Dispatch Queue
dispatch_queue_t globeQueue = dispatch_get_global_queue(<#long identifier#>,
<#unsigned long flags#>)
- 所有应用程序都能够使用的 Concurrent Dispatch Queue
- 优先级:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高优先级
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认优先级
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低优先级
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台优先级
- ⚠️:通过XNU内核用于Global Dispatch Queue的线程不能保证实时性能,因此执行优先级只是大致的判断
使用:
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,
0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,
0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,
0);
- 2.4、dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
});
⚠️在多长时间之后执行,这是我们常用的API之一,现在说一下需要注意的点:
- dispatch_after函数不是在指定时间后执行处理,而是在指定时间追加处理到 Dispatch Queue;
- 本身存在延迟:执行的时间 = 传入参数 + runloop循环1次的时间
- 2.5、dispatch_once
- (instancetype)init {
if (self = [super init]) {
static int initialized = NO;
if (initialized == NO) {
// 初始化单例
}
initialized = YES;
}
return self;
}
- (instancetype)init {
if (self = [super init]) {
static dispatch_once_t pred;
dispatch_once(&pred, ^{
// 初始化单例
});
}
return self;
}
第一段代码存在的隐患:
如果在多核CPU中,在正在更新表示是否初始化的标志变量时读取,就有可能多次执行初始化处理。
- 2.6、Dispatch Group
使用场景:在追加到Dispatch Queue中多个处理全部结束后想执行结果处理。也就是说等多个异步线程执行完成后执行
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_async(queue, ^{
NSLog(@"执行一");
});
dispatch_async(queue, ^{
NSLog(@"执行二");
});
dispatch_async(queue, ^{
NSLog(@"执行三");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"三条线程执行完成");
});
2019-10-07 12:24:22.914339+0800 GCD[10825:325226] 执行二
2019-10-07 12:24:22.914345+0800 GCD[10825:325223] 执行一
2019-10-07 12:24:22.914369+0800 GCD[10825:325224] 执行三
2019-10-07 12:24:22.930029+0800 GCD[10825:325078] 三条线程执行完成
也可以使用 dispatch_group_wait函数仅等待全部处理执行结束
dispatch_group_wait(<#dispatch_group_t _Nonnull group#>,
<#dispatch_time_t timeout#>)
- 参数一、传入线程组
- 参数二、指定等待的时间 (DISPATCH_TIME_FOREVER意味着永久等待)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_async(queue, ^{
NSLog(@"执行一");
});
dispatch_async(queue, ^{
NSLog(@"执行二");
});
dispatch_async(queue, ^{
NSLog(@"执行三");
});
sleep(3);
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"三条线程执行完成");
- 2.7、dispatch_barrier_async(栅栏函数)
在进程管理中起到一个栅栏的作用,它等待所有位于barrier函数之前的操作执行完毕后执行,并且在barrier函数执行之后,barrier函数之后的操作才会得到执行,
⚠️该函数需要同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"执行一");
});
dispatch_async(queue, ^{
NSLog(@"执行二");
});
dispatch_barrier_async(queue, ^{
sleep(3);
NSLog(@"xxxxxxxxxx");
});
dispatch_async(queue, ^{
NSLog(@"执行三");
});
dispatch_async(queue, ^{
NSLog(@"执行四");
});
2019-10-07 12:48:55.348575+0800 GCD[11221:340677] 执行一
2019-10-07 12:48:55.348582+0800 GCD[11221:340680] 执行二
2019-10-07 12:48:58.350728+0800 GCD[11221:340680] xxxxxxxxxx
2019-10-07 12:48:58.350999+0800 GCD[11221:340678] 执行四
2019-10-07 12:48:58.351001+0800 GCD[11221:340680] 执行三
-
2.8、dispatch_set_target_queue
更改Dispatch Queue的执行优先级 -
2.9、dispatch_apply
该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(5, queue, ^(size_t index) {
NSLog(@"------%zu", index);
});
NSLog(@"执行完成");
2019-10-07 16:18:05.663250+0800 GCD[12009:372598] ------0
2019-10-07 16:18:05.663277+0800 GCD[12009:372679] ------1
2019-10-07 16:18:05.663277+0800 GCD[12009:372683] ------2
2019-10-07 16:18:05.663302+0800 GCD[12009:372678] ------3
2019-10-07 16:18:05.663446+0800 GCD[12009:372679] ------4
2019-10-07 16:18:05.663581+0800 GCD[12009:372598] 执行完成
⚠️:由于dispatch_apply函数也与 dispatch_sync函数相同,会等待处理执行结束,因此推荐在dispatch_async函数中非同步地执行dispatch_apply函数
- 3.0、dispatch_suspend 和 dispatch_resume
-
3.1、Dispatch Semaphore
持有计数信号
三、关于GCD的面试题
- 你理解的多线程?
- iOS的多线程方案有哪几种?你更倾向于哪一种?
- 你在项目中用过GCD么
- GCD的队列类型
- 说一下OperationQueue和GCD的区别,以及各自的优势
- 线程安全的处理手段有哪些?
- OC你了解的锁有哪些?在你回答基础上进行二次提问?
自旋锁和互斥锁对比?使用以上锁需要注意哪些?使用C/OC/C++,任选其一,实现自旋或互斥?口述即可
- 3.1、iOS的多线程方案有哪几种?你更倾向于哪一种?
- 3.2、自旋锁、互斥锁比较
什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
- 3.3、关于死锁的相关面试题
NSLog(@"执行任务1");
dispatch_queue_t main = dispatch_get_main_queue();
dispatch_sync(main, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
2019-10-07 10:30:04.809292+0800 GCD[10060:304837] 执行任务1
❌产生死锁
NSLog(@"执行任务1");
dispatch_queue_t main = dispatch_get_main_queue();
dispatch_async(main, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
2019-10-07 10:29:03.893940+0800 GCD[10038:303891] 执行任务1
2019-10-07 10:29:03.894175+0800 GCD[10038:303891] 执行任务3
2019-10-07 10:29:03.918505+0800 GCD[10038:303891] 执行任务2
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.xxx", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
2019-10-07 10:31:09.843690+0800 GCD[10091:306352] 执行任务1
2019-10-07 10:31:09.843905+0800 GCD[10091:306352] 执行任务5
2019-10-07 10:31:09.843937+0800 GCD[10091:306413] 执行任务2
❌产生死锁
通过上述代码我们可以得出一个结论:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列。
如果某些场景需要这样做,可以使用两个队列来避免上面产生死锁问题。
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
后续待扩展(线程锁)、GNUstep
这篇文章写的非常棒,可以看看 iOS多线程--彻底学会多线程之『GCD』