iOS八股文(十二)GCD之函数和死锁源码浅析
面试题
还是一样,先看两道面试题。
第一题
//经典面试题
- (void)interview04 {
//全局队列
dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
__block int a = 0;
while (a < 100) {
dispatch_async(globQueue, ^{
// NSLog(@"内部: %d - %@",a,[NSThread currentThread]);
a++;
});
};
NSLog(@"外部打印_____ %d",a);
}
问最后的打印结果是多少。
其实这道题是我真实在面试过程中遇到的的,当时面试的while
条件是a<5
,我当时第一次遇到还是比较懵,一直在纠结是不是5
,思考片刻也没有答上来。
首先结果只有3个,等于100
,大于100
,大于等于100
。等于100的情况肯定是有可能发生的。所以就看有没有可能发生大于100的情况。代码走出while循环的时候a=100
,就看在NSLog
打印的时候还回不回异步执行a++
了。再看a++
的操作是在其他线程异步完成的(异步函数+全局队列),也就是说,有可能来到了下一次循环的while
判断,上一次循环的a++
还没有执行。所以a++
执行的次数应该是大于等于100
的。
第二题
第二道题是第一道题的变种:
- (void)interview05 {
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
__block int a = 0;
for (int i = 0; i < 100; i++) {
dispatch_async(globalQueue, ^{
a++;
});
}
NSLog(@"外部答应_____ %d",a);
}
这题是a++
只有100次,就看在NSlog
执行的时候这100个a++
是否都执行完了。同样是异步执行,极限情况下是有可能执行完成的,这样打印的就刚好是100,如果有没有执行完的a++
,那么打印出来的值有可能小于100.
死锁的底层原理
死锁crash分析
上文讲过使用同步函数往当前串行队列添加任务会产生死锁。我们先debug出crash的堆栈调用关系,然后找到对应dispatch源码
中的位置,进行分析:
可以看到如果满足其条件就会触发crash。再看条件中函数的实现:
这里可以着重分析下这段代码和这一句注释。先说下我最终得到的结论,这里其实是判断lock_value 和tid 的第2位到第31位(后30位)是否相等,先贴2个定义:
_dispatch_lock_owner(lock_value)
的操作其实是lock_value
后2位抹0处理。
^
为异或,相同为0,不同为1,如果两者高30位相同,结果为高30位为0,而后面与运算0xfffffffc
,其实是忽略二者后2位的比较结果。
其中tid
,thread ID
线程编号,而lock_value
是通过队列获取的。获取代码如下:
这里比较难懂,但其实源码阅读到这里已经对其中原理有一定的认知了。
当
dispatch_sync
+串行队列
的时候,这个串行队列就会对应一个线程
,如果添加任务的代码执行的线程
,和串行队列所对应的线程
是一个线程的时候,就会发生死锁
,从而crash。
死锁示例
例如:
- (void)interview033 {
dispatch_queue_t queue = dispatch_queue_create("com.osDemo.serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"---任务1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"---任务2");
});
NSLog(@"---任务3");
});
}
这段代码是会发生死锁的,main_queue
对应的线程是主线程,而dispatch_sync(dispatch_get_main_queue()
执行也是在主线程执行的,所以发生死锁。
-(void)interview011{
dispatch_queue_t queue = dispatch_queue_create("com.demo.queue", DISPATCH_QUEUE_SERIAL);
NSLog(@"test____1"); // 任务1
dispatch_async(queue, ^{
NSLog(@"test____2"); // 任务2
dispatch_sync(queue, ^{
NSLog(@"test____3"); // 任务3
});
NSLog(@"test____4"); // 任务4
});
usleep(20);
NSLog(@"test____5"); // 任务5
}
这段代码同样会发生死锁,dispatch_sync(queue
执行的是在一个自线程中,而queue
此时对应的线程也是该线程,所以死锁。
同步函数源码分析
找到源码中同步函数的实现:
其中unlikely为小概率事件,我们看他执行原理的时候,先跟这大概率的分支去分析。按照这个思路,我们一层一层往里找。
注意这里,调用了
_dispatch_barrier_sync_f
这个从名字看,最终调用了栅栏函数。我们继续看栅栏函数实现,这里的参数func
就是我们外面的block任务
。
这里
func
直接传入到离其他方法内部,继续往里面点:
直接看和
func
有关的函数:
走过千山万水,终于来到了最我们
block
的执行的地方,这里可以看到block
是直接执行了,所以遇到同步函数,我们可以粗暴的理解为,里面的任务马上就要执行。
也可以在block内部
打断点,通过lldb bt
命令,查看调用堆栈,同样可以找到同步函数的调用关系:
这里就有疑问了,为什么会调用barrier
函数?
如果是并发队列,岂不是会执行完之前的任务,才会执行当前任务么?
又对如下代码进行了打印测试:
这时候发现并没有走
barrier
函数:
这里再次回到barrier
调用的源码部分:
可以看到
barrier
调用的条件是dq_width == 1
,上文我们也了解到,只有串行队列的dq_width
才为1,故如果是串行队列走上面的分支,如果是并发队列走下面的分支。
从调试代码中,我们可以把
同步函数
+并发队列
这种情况,理解为在并发队列中进行插队
。并不会等之前的任务执行完成,再执行这个同步任务,而是优先
执行同步任务。 但在串行队列
中,同步函数相同于栅栏函数
,会等待
队列中之前的任务完成之后再执行当前任务。可以理解为串行队列不允许有插队行为。
异步函数的源码分析
在dispatch源码中找到定义:
点进去:
继续看有关
work
的操作:
这里是将外面传入的
block
进行了封存。
注意这里的
dx_push
,在宏定义中找到其定义
这里是根据不同种类的队列,执行不同的函数。(读到这感觉平时写的
if else
switch
有点low)。
继续查找了这些方法,发现也没有对block
的调用相关的代码。只能通过lldb的方式来查看block
的调用堆栈了。
发现跟同步函数一样,都是通过
_dispatch_client_callout
触发的。而在这之前是有很多的线程操作。
可以对异步函数的特点总结:
- 将任务存储,不立即执行
- 有开辟线程的能力
转载自:https://juejin.cn/post/7102338284352700423