一.解题
今天在群里发现了一道比较有意思的题, 仔细想了一下, 还是值得深思的, 所以写出来记录一下, 以下均为本人对多线程的理解, 如果有误请留言指出, 我会第一时间进行改成.
题干:
__block int x = 0;
__block int y = 0;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d", x++);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"%d", ++y);
});
NSLog(@"%d", y++);
NSLog(@"%d", x + y);
while (1) {}
});
那么下面直接解答
1.首先调用dispatch_async
(异步调度)把我们的任务抛到dispatch_get_global_queue
(全局并行队列), 下文称gq
.
2.打印NSLog(@"%d", x++);
, 由于++
在x
后面, 分号结束才自增1, 所以打印出0.
3.调用dispatch_sync
把NSLog(@"%d", ++y);
任务抛到主线程队列中, 主线程在执行完其他任务后就会执行该任务, 不死锁的原因是dispatch_sync
并没有在主线程中创建, 而是在dispatch_get_global_queue
中创建并等待任务执行结束, gcd把任务抛到了主线程队列, 队列处理完这个任务, 这个等待函数dispatch_sync
会自然结束等待状态, 继续进行下面的任务.
4.下面就都是简单的逻辑运算了, 直到碰上while (1) {}
死循环.
下面是答案
2019-03-07 15:21:14.471910+0800 [75481:2405428] 0
2019-03-07 15:21:14.476663+0800 [75481:2405369] 1
2019-03-07 15:21:14.476834+0800 [75481:2405428] 1
2019-03-07 15:21:14.476986+0800 [75481:2405428] 3
二.拓展
通过上面的问题我又拓展一下关于多线程
和GCD
的知识, 你如果没看懂上面的解答, 不妨来看一下.
这道题真正疑点是为什么没有造成死锁呢?
题目运算上是没有难度的, 无非是++x和x++的区别, ++x会直接+1, x++会在分号结束后+1, 然而加入了GCD, 这就涉及到了多线程, 队列, 同步异步的问题, 那我就把这些东西从前到后, 由浅入深, 完完整整的说一遍.
按照我的理解:
一个程序中只存在一个主线程, 主线程可以帮助我们完成任何事情(创建UI, 网络请求, 切换界面), 但是主线程有个弊端, 就是每次只能处理一个任务, 处理完成后才会处理第二个任务, 比如说你既想在下载图片
又想切换UI界面
, 在你点击下载图片的那一刻后屏幕就没有反应了, 直到图片下载完成你才能切换界面, 这样显然是不符合用户需求的, 所以苹果在iOS中引入了多线程, 就是为了解决处理耗时任务会导致主线程卡住
的问题, 这个卡住
的现象就叫做线程阻塞, 解决方案就是加入子线程和异步处理, 这些子线程的好处就是不会阻塞主线程(异步处理), 所以利用子线程可以帮助你完成一些耗时的操作.
通常上网络上对死锁的解释是
主线程队列是串行队列, 队列中的任务是一个执行完成后才去执行另一个, 用同步方法将任务1提交到主线程队列就会阻塞住主线程, 而这个刚提交的任务1必须等待主线程中的任务执行完毕才可以执行, 但这时主线程已经被阻塞了, 就是说要等任务1执行完成后才能去执行原有的任务, 所以双方在互相等待而卡住, 最后一个任务也没机会执行到, 就造成了死锁.
针对上面这个解释我觉得非常贴切, 下面我用我的理解来补全一下
首先是串行队列和并行队列:
队列的概念:队列就跟我们排队一样, 从类型上一共分为两种串行队列和并行队列.
串行队列: 10个人在售票窗口买票, 中途不能插队, 任务是按照顺序进行的, 第一个执行完了才继续执行第二个一直到第10个.
并行队列: 10个人在10个自动售货机前买票, 同时进行, 至于谁先买完, 我们无从得知...
然后说一下GCD
, 平时常用的调度方法包括下面两种dispatch_sync
和dispatch_async
下面我们就依次介绍一下.
我们首先来看一下iOS中的线程
主线程
: 应用只有一个, 编号为1且名称为main, 同步处理耗时任务会阻塞.
子线程
: 应用中可能有多个, 编号不确定可能为1也可能不为1, 处理耗时操作不会阻塞.
dispatch_sync
: 翻译为同步调度, 在指定队列中扔进去一个任务(block), 该任务由主线程亲自处理, 所以处理耗时任务一定会阻塞
dispatch_async
: 翻译为异步调度, 在指定队列中扔进去一个任务(block), 该任务有可能由主线程或子线程负责处理, 所以有可能会阻塞(※在主线程队列中就会让主线程进行异步处理, 事实胜于雄辩!)
所以在这里我结合队列总结一下, 在我们的程序中一共存在4种队列(我知道的), 分别是:
下文称同步调度为同步, 异步调度为异步
1.主线程队列
dispatch_get_main_queue()
主线程队列是一个典型的串行队列, 里面最多只能容许一个线程来执行, 也就是主线程, 向里面插入任务, 无论是同步或异步, 该任务均由`主线程`执行, 但在主线程运行的队列中同步调度会死锁.
2.全局并行队列
dispatch_get_global_queue(0, 0)
全局并行队列是一个典型的并行队列.
3.串行队列
dispatch_queue_create("com.objcat.serial", DISPATCH_QUEUE_SERIAL);
串行队列是自己创建出的队列, 主线程队列一样, 任务也是一个一个来执行的.
4.并行队列
dispatch_queue_create("com.objcat.concurrent", DISPATCH_QUEUE_CONCURRENT);
并行队列是自己创建出的队列, 跟全局并行队列一样, 不同线程上的任务是一起执行的, 哪个先执行完并不一定.
2, 3, 4 都算上
向非主线程队列中里面插入任务, 任务在哪个线程执行与`同步或异步`有关, 如果是同步, 主线程会来执行任务, 如果是异步子线程会来执行, 同步可能会阻塞, 异步不会阻塞, 和主线程队列的区别是, 主线程队列如果异步插入任务也是主线程来执行的, 因此即使执行耗时任务也不会阻塞, 那这个知识点就科普到这里.
所以到这里你应该会明白一个道理, 是否阻塞与
线程的种类
无关, 与队列的种类
也无关, 与主线程是否同步执行耗时任务有关系, 同步任务一定会交给主线程执行, 如果该任务耗时则必阻塞, 异步任务可能由主线程或子线程执行, 并且永不阻塞.
就这样我把dispatch_sync
和dispatch_async
也都解释了, 你们也烦了, 接下来我们来讲一个故事, 告诉你为什么会死锁, 我在网上找到了这么一段故事, 并改编了一下.
假设我们当前主线程队列中在执行一个运钞任务, 任务是把钞票从A送到B, 接到任务后车立刻就出发了, 走到半路的时候, 领导突然来电话说B的金库满了, 想改路送到C, 任务很紧急我们保持通话直到你送到为止(sync阻塞), 所以到这里任务就改变了, 新任务是把钞票送到C, 但是它现在还不能开车, , 它在等钞票A送到B, 当然这是不可能等到的, 因为它始终在原地....
网上还有这张图, 也给你拿来了
我们可以看到任务3卡在了任务2之前并阻塞了线程, 而任务2在等任务3, 任务3在等任务2, 所以就造成了死锁.
说道这里, 你再回去读一读题目或许就会明白了, 确实有点绕但仔细分析并不难理解.
三.反思
有可能你认为刚才讲的故事不是特别通顺, 没错, 我同样认为在很多地方仍解释不通, 假如当前主线程队列中不存在任务, 我向其中插入一个任务为什么就不能执行呢?
我尝试在死锁前面打断点来查看主线程队列中的任务
结果我发现任务是viewDidLoad
, 证明确实有任务正在进行.
这样就可以解释通, 主线程一直在处理viewDidLoad
的代码, 所以当我们强行插入一个任务的时候主线程队列就会因为这个任务的强行插入而转为互相等待状态因此会死锁.
四.参考文章
五个案例让你明白GCD死锁
http://ios.jobbole.com/82622/
彻底搞懂OC中GCD导致死锁的原因和解决方案
https://blog.csdn.net/abc649395594/article/details/48017245
网友评论