iOS 多线程总结(上)
一、前言
多线程是在 iOS 里非常重要的一块儿知识点,我最近学习了李明杰大神的多线程相关视频,对自己的多线程相关知识进行了查缺补漏,受益良多,在此根据所学进行简单的记录,希望能帮助更多伙伴,也能作为自己模糊了以后查阅使用。 本篇文章分为上下两篇。
二、iOS中常见的多线程方案
技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | ☑ 一套通用的多线程API ☑ 适用于Unix\Linux\Windows等系统☑ 跨平台\可移植☑ 使用难度大 | C | 程序员管理 | 几乎不用 |
NSThread | ☑ 使用更加面向对象☑ 简单易用,可直接操作线程对象 | OC | 程序员管理 | 偶尔使用 |
GCD | ☑ 旨在替代NSThread等线程技术☑ 充分利用设备的多核 | C | 自动管理 | 经常使用\color{red}{经常使用}经常使用 |
NSOperation | ☑ 基于GCD(底层是GCD)☑ 比GCD多了一些更简单实用的功能☑ 使用更加面向对象 | OC | 自动管理 | 经常使用\color{red}{经常使用}经常使用 |
二、同步、异步、串行、并发
1、GCD 中有 2 个用来执行任务的函数
- 用同步的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
☑ queue:队列 ☑ block:任务
2、GCD 的队列可以分为 2 大类型
-
并发\color{red}{并发}并发队列(Concurrent Dispatch Queue) ☑ 可以让多个任务并发\color{blue}{并发}并发(同时)执行(自动开启多个线程同时执行任务) ☑ 并发\color{blue}{并发}并发功能只有在异步\color{blue}{异步}异步(dispatch_async\color{blue}{async}async)函数下才有效
-
串行\color{red}{串行}串行队列(Serial Dispatch Queue) ☑ 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
-
用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
-
GCD 源码:github.com/apple/swift…
3、容易混淆的术语
-
有 4 个术语比较容易混淆:同步、异步\color{blue}{同步、异步}同步、异步、并发、串行\color{red}{并发、串行}并发、串行
-
同步\color{blue}{同步}同步和异步\color{blue}{异步}异步主要影响:能不能开启新的线程 ☑ 同步\color{blue}{同步}同步:在当前\color{green}{当前}当前线程中执行任务,不具备\color{green}{不具备}不具备开启新线程的能力 ☑ 异步\color{blue}{异步}异步:在新的\color{green}{新的}新的线程中执行任务,具备\color{green}{具备}具备开启新线程的能力
-
并发和串行主要影响:任务的执行方式 ☑ 并发\color{red}{并发}并发:多个\color{blue}{多个}多个任务并发(同时)执行 ☑ 串行\color{red}{串行}串行:一个\color{blue}{一个}一个任务执行完毕后,再执行下一个任务
dispatch_sync
是立马在当前线程执行完里面的任务。\color{red}{是立马在当前线程执行完里面的任务。}是立马在当前线程执行完里面的任务。
dispatch_sync
和 dispatch_async
用来控制是否要开启新的线程。
队列的类型,决定了任务的执行方法(并发、串行) 1、并发队列 2、串行队列 3、主队列(也是一个串行队列)
4、各种队列的执行效果:
并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步( sync ) | 没有\color{red}{没有}没有开启新线程串行\color{blue}{串行}串行执行任务 | 没有\color{red}{没有}没有开启新线程串行\color{blue}{串行}串行执行任务 | 没有\color{red}{没有}没有开启新线程串行\color{blue}{串行}串行执行任务 |
异步( async ) | 有\color{green}{有}有开启新线程并发\color{orange}{并发}并发执行任务 | 有\color{green}{有}有开启新线程串行\color{blue}{串行}串行执行任务 | 没有\color{red}{没有}没有开启新线程串行\color{blue}{串行}串行执行任务 |
- 使用 sync 函数往当前串行\color{red}{当前}\color{blue}{串行}当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
5、问题
问题1:
以下代码是在主线程执行的,会不会产生死锁?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
答案:会!执行顺序是1崩溃。
分析:
1、队列的特点:排队,FIFO
(Fisrst In First Out,先进先出)
2、dispatch_sync
同步队列特点:立马在当前线程执行任务,执行完毕才能继续往下执行。
因为任务 2 是在同步执行,并且在主队列中。而 viewDidLoad 也是在主队列中。
所以主队列中的任务 2 会等 viewDidLoad 这个任务先执行完再执行,而 viewDidLoad 这个任务需要执行完任务 3 才算执行完。所以就出现了死锁。
如下图所示:
问题2:
以下代码是在主线程执行的,会不会产生死锁?(和第1题的区别只是把sync改为了async)
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
答案:不会!执行顺序是132。 分析:异步虽然因为是主队列不会开启新线程,但因为是异步的,所以不是必须马上取出任务2执行再执行后面的任务3,所以可以最后再取出任务2执行。
问题3:
以下代码是在主线程执行的,会不会产生死锁?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // Block 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // Block 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
答案:会!执行顺序是152崩溃。 分析:dispatch_sync是立马在当前线程执行完里面的任务。\color{red}{是立马在当前线程执行完里面的任务。}是立马在当前线程执行完里面的任务。 代码中注释标明了 Block0\color{blue}{Block 0}Block0 和 Block1\color{green}{Block 1}Block1。 要想执行完Block0\color{blue}{Block 0}Block0,必须执行完Block1\color{green}{Block 1}Block1(因为是sync)才能执行后面的任务4,才算执行完Block0\color{blue}{Block 0}Block0。但要想执行Block1\color{green}{Block 1}Block1,又必须先执行队列里先进入的Block0\color{blue}{Block 0}Block0(FIFO)。Block0\color{blue}{Block 0}Block0 和 Block1\color{green}{Block 1}Block1互相等待,所以造成了死锁。
问题4:
以下代码是在主线程执行的,会不会产生死锁?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // Block 0
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{ // Block 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
答案: 不会!执行顺序是15234。 分析: 因为Block0\color{blue}{Block 0}Block0 和 Block1\color{green}{Block 1}Block1所处两个不同队列。就算这道题两个都是串行队列,也不会产生死锁。
问题5:
以下代码是在主线程执行的,会不会产生死锁?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // Block 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // Block 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
答案: 不会!执行顺序是15234。 分析: 因为Block0\color{blue}{Block 0}Block0 和 Block1\color{green}{Block 1}Block1处在并发队列,并发队列不会阻塞。
- 总结: 使用 sync 函数往当前串行\color{red}{当前}\color{blue}{串行}当前串行队列中添加任务,会卡住当前的串行队列(产生死锁) 死锁 的产生两个条件: 1、是 sync 同步的; 2、往当前的串行\color{red}{当前}的\color{blue}{串行}当前的串行队列添加任务;
6、多线程的安全隐患
-
资源共享 ☑ 1 块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源\color{red}{多个线程可能会访问同一块资源}多个线程可能会访问同一块资源 ☑ 比如多个线程访问同一个对象、同一个变量、同一个文件。
-
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全\color{red}{数据错乱和数据安全}数据错乱和数据安全问题
7、面试题
面试题1:
请问下面代码的打印结果是什么?
打印结果是:1、3
原因:
1、
performSelector:withObject:afterDelay:
的本质是往 Runloop 中添加定时器。
2、子线程默认没有启动 Runloop。
如果换成 performSelector:withObject
则打印结果为 132,这个不带 Delay 的方法不是 runloop 的,相当于直接调用。
runloop 的代码是没有开源的,所以看不到 afterDelay 的实现,如果想了解,可以看 GNUstep。
- GNUstep GNUstep 是 GNU 计划的项目之一,它将 Cocoa 的 OC 库重新开源实现了一遍。 源码地址:www.gnustep.org/resources/d… 虽然 GNUstep 不是苹果官方源码,但还是具有一定的参考价值。
面试题2:
请问下面代码的打印结果是什么?
打印结果是: 1 崩溃
分析:在执行 performSelector 的时候,目标线程已经退出了,所以崩溃了。可以在block 中开启 runloop:
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
这样就会打印 1 和 2 了。因为里面启动了一个 runloop,执行完打印 1 以后线程并没有死,而是 runloop 休眠了,之后 performSelector 唤醒 runloop 去执行 test。
以上的总结参考了并部分摘抄了以下文章,非常感谢以下作者的分享!: 小马哥-李明杰的《多线程》课程
转载请备注原文出处,不得用于商业传播——凡几多
转载自:https://juejin.cn/post/6992936105532194853