我是靠谱客的博主 长情可乐,最近开发中收集的这篇文章主要介绍单例dispatch_once造成的死锁按照解决deadlock的一般思路,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

好久没有更新了,这一次遇到一个单例模式造成的死锁,比较有代表性,这里做一个总结,分享给大家

起初,我们发现程序偶现死锁的问题,

按照解决deadlock的一般思路

是找到问题发生时,访问同一资源或者数据结构的可疑线程

OC和C有很多的基础类型都是线程不安全的,比如NSDictionary、array等,

结果一无所获????

看来问题没有这么简单????

那就找,问题发生时,访问同一个方法的可疑线程

经过几次的信息获取,合并同类项,终于发现了这几个死锁的共同特性(),

即总会同时出现以下两个堆栈:

通过上图,我们可以看出,两个线程都对telephonyNetworkInfo进行了访问,一个是主线程,一个是子线程,会不会这里出现了问题,查看telephonyNetworkInfo问题

这段代码很简单,世界上的单例基本上都是这么写的????

那这里会不会有问题呢

这里有一个关键的信号量,dispatch_once,会对后面的任务进行堵塞

Apple对于dispatch_once的源码地址

简化实现的原理是:
1、dispatch_once不止是简单的执行一次,如果再次调用会进入非首次更改的模块,如果有未DONE的请求会被添加到链表中
2、所以dispatch_once本质上可以接受多次请求,会对此维护一个请求链表
3、如果在block执行期间,多次进入调用同类的dispatch_once函数(即单例函数),会导致整体链表增长。

看完这三点,找其中可能引起死锁的地方,大家可以先思考一下

????

????

????

首先想到:如果里面的方法再次调用dispatch_once是否会造成永久性死锁?

答案是肯定的,https://www.jianshu.com/p/58f5fb01ae4f这篇文章的例子就形象的说明了这个问题

但这并不是这次问题的原因,因为这个问题并没有循环调用

然后想到:后面进来的线程会被堵塞在这里,如果先进入的线程与后面堵塞的线程有一些交互,那会不会也造成永久性死锁?

答案也是肯定的,而且在实际业务中,绝大部分是这样的死锁。

https://www.jianshu.com/p/8b8abae1b32f这篇文章讲的就是一个典型的案例

 

再次回到这个问题,一个是主线程,一个是子线程,都是进行CTTelephonyNetworkInfo的初始化,如果子线程先进来,主线程在堵塞,那CTTelephonyNetworkInfo初始化时会不会与主线程交互呢?

经过相关资料的查找发现CTTelephonyNetworkInfo的初始化比较复杂

比如下面的堆栈:

0   __psynch_cvwait + 8
1   _pthread_cond_wait + 640
2   -[__NSOperationInternal _waitUntilFinished:] + 132
3   -[__NSObserver _doit:] + 232
4   __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 20
5   _CFXRegistrationPost + 400
6   ___CFXNotificationPost_block_invoke + 60
7   -[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1504
8   _CFXNotificationPost + 376
9   -[NSNotificationCenter postNotificationName:object:userInfo:] + 68
10  -[CTTelephonyNetworkInfo queryDataMode] + 408
11  -[CTTelephonyNetworkInfo init] + 336
12  -[OTPolicyCenter init] (NWPolicyCenter.m:52)
13  __31+[NWPolicyCenter sharedInstance]_block_invoke (NWPolicyCenter.m:43)
14  _dispatch_client_callout + 16
15  dispatch_once_f + 56
16  +[OTPolicyCenter sharedInstance] (once.h:68)
17  +[OTUtils singletonObject:getter:] (OTUtils.m:271)
...

我们可以看到CTTelephonyNetworkInfo init时向主线程发出了操作: [__NSOperationInternal _waitUntilFinished:] 。如果主线程在阻塞中等待  onceToken ,所以主线程不能接收子线程的通知,于是子线程一直在等主线程接受通知,也不会去释放  onceToken ,死锁生成。 

至于为什么 [NSNotificationCenter postNotificationName:object:userInfo:] 会同步等待主线程返回,猜测苹果自己在实现中接收通知是这样做的,要求接收通知的block在mainQueue上执行,比如: 

[[NSNotificationCenter defaultCenter]
  addObserverForName:NotificationName
              object:nil
               queue:[NSOperationQueue mainQueue]
          usingBlock:^(NSNotification *ns) {
              NSLog(@"Notification %@", ns);
}];

问题找到了,解决方案也比较简单,无非两种,一种是不允许子线程访问CTTelephonyNetworkInfo方法,一种是不使用单例的方式,改成静态变量,这就涉及具体业务,我们选择了后者,一方面是因为业务上的限制,需要子线程调用,另外,该方法是一个基础服务方法,调用的地方比较多,走查代码工作量大,且有稳定性的隐患;另一方面从性能消耗的角度上讲,将单例模式改为静态变量,对于实时服务的代码来讲,性能消耗差不多。

问题解决!

参考资料:

https://blog.csdn.net/fishmai/article/details/72723677 dispatch_once造成的死锁----分析、解决与自动检测

https://www.jianshu.com/p/8b8abae1b32f 30行代码演示dispatch_once死锁

https://www.jianshu.com/p/58f5fb01ae4f 单例滥用 - dispatch_once死锁造成crash

拓展知识:

是否能在研发流程内避免这里问题或者提前发现这类的问题,笔者先抛砖引玉

关于发现问题

1.在线程申请加锁和解锁once token时,对线程打标记:
自己的代码中可以用宏定义改掉dispatch_once的实现,在其中对线程打标记,这个应该不难。
别人的代码中只能在运行时里面换出sharedInstance, defaultManager等方法来打标记。
2.找出子线程准备锁主线程的位置:
仅可以 hook objective-c 实现的同步方法,不能 hook GCD 的同步方法,所以仍要靠人肉review,而且只能review自己代码,不能review SDK。
3.制订子线程锁主线程强制CR和文档登记制度,从项目规则上避免问题的发生

最后

以上就是长情可乐为你收集整理的单例dispatch_once造成的死锁按照解决deadlock的一般思路的全部内容,希望文章能够帮你解决单例dispatch_once造成的死锁按照解决deadlock的一般思路所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(47)

评论列表共有 0 条评论

立即
投稿
返回
顶部