dispatch_once造成的死锁----分析、解决与自动检测

来源:互联网 发布:mac上编写c语言的软件 编辑:程序博客网 时间:2024/06/05 21:00

现象

最近遇到了一个死锁crash,主线程在dispatch_once时卡住了:

Thread 0 name:  Dispatch queue: com.apple.main-threadThread 0 Crashed:0   __ulock_wait + 81   _dispatch_unfair_lock_wait + 482   _dispatch_gate_wait_slow + 563   dispatch_once_f + 1244   +[OTPolicyCenter sharedInstance] (once.h:68)7   +[OTWebViewUtil completeUrlScheme:] (WVWebViewUtil.m:26)...30  start + 4

卡死的代码很简单,世界上的单例基本上都是这么开的:

+ (OTPolicyCenter *)sharedInstance{    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        policyCenterInstance = [[OTPolicyCenter alloc] init];    });    return policyCenterInstance;}

其他线程大多数也都卡住了(除了带runloop的线程和事情还没做完的线程):

Thread 1:0   __psynch_cvwait + 81   _pthread_cond_wait + 6402   -[__NSOperationInternal _waitUntilFinished:] + 1323   -[__NSObserver _doit:] + 2324   __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 205   _CFXRegistrationPost + 4006   ___CFXNotificationPost_block_invoke + 607   -[_CFXNotificationRegistrar find:object:observer:enumerator:] + 15048   _CFXNotificationPost + 3769   -[NSNotificationCenter postNotificationName:object:userInfo:] + 6810  -[CTTelephonyNetworkInfo queryDataMode] + 40811  -[CTTelephonyNetworkInfo init] + 33612  -[OTReachability networkStatusForFlags:] (AFReachability.m:216)...25  start_wqthread + 4Thread 4:0   __semwait_signal + 81   nanosleep + 2122   usleep + 643   wpthread_main + 2164   _pthread_body + 2405   _pthread_body + 06   thread_start + 4Thread 7 name:  Dispatch queue: com.apple.root.default-qosThread 7:0   semaphore_wait_trap + 81   _dispatch_semaphore_wait_slow + 2162   CFURLConnectionSendSynchronousRequest + 2843   +[NSURLConnection sendSynchronousRequest:returningResponse:error:] + 1204   +[UTMCHttpHelper post:url:dict:len:errorCode:] + 18645   __39-[UTMCOnlineConfManager syncOnlineconf]_block_invoke + 6606   _dispatch_call_block_and_release + 247   _dispatch_client_callout + 168   _dispatch_queue_override_invoke + 7329   _dispatch_root_queue_drain + 57210  _dispatch_worker_thread3 + 12411  _pthread_wqthread + 128812  start_wqthread + 4Thread 18 name:  Dispatch queue: com.apple.NSURLSession-workThread 18:0   __psynch_cvwait + 81   _pthread_cond_wait + 6402   -[__NSOperationInternal _waitUntilFinished:] + 1323   -[__NSObserver _doit:] + 2324   __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 205   _CFXRegistrationPost + 4006   ___CFXNotificationPost_block_invoke + 607   -[_CFXNotificationRegistrar find:object:observer:enumerator:] + 15048   _CFXNotificationPost + 3769   -[NSNotificationCenter postNotificationName:object:userInfo:] + 6810  -[CTTelephonyNetworkInfo queryDataMode] + 40811  -[CTTelephonyNetworkInfo init] + 33612  -[OTPolicyCenter init] (NWPolicyCenter.m:52)13  __31+[NWPolicyCenter sharedInstance]_block_invoke (NWPolicyCenter.m:43)14  _dispatch_client_callout + 1615  dispatch_once_f + 5616  +[OTPolicyCenter sharedInstance] (once.h:68)17  +[OTUtils singletonObject:getter:] (OTUtils.m:271)...43  start_wqthread + 4

原因

在主线程中卡死前的一行  [OTPolicyCenter sharedInstance] ,在线程18中也找到了相同的调用。 

再来看一眼这个简单的单例方法:

+ (OTPolicyCenter *)sharedInstance{    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        policyCenterInstance = [[OTPolicyCenter alloc] init];    });    return policyCenterInstance;}

线程18首先进入了  sharedInstance ,在 dispatch_once(&onceToken, ^) 时锁住了  onceToken ,主线程稍后进入 sharedInstance ,阻塞在  dispatch_once(&onceToken, ^) 这里,而线程18继续往下执行到  [[OTPolicyCenter alloc] init] 。 

此时线程18阻塞式的向主线程发出了操作: [__NSOperationInternal _waitUntilFinished:] 。因为主线程在阻塞中等待  onceToken ,所以主线程不能接收线程18的通知,于是线程18一直在等主线程接受通知,也不会去释放  onceToken ,死锁生成。 

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

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

当然此时线程18上如果不是发了一个阻塞式的通知,而是做了一些其他的需要在主线程执行并同步返回的事,也会造成死锁。

解决方案

  1. 自动解决或加保护?

    如果围绕自动解决或者加保护的方式来做,禁止子线程同步调用主线程也好是不现实的(总有业务限制),禁止子线程和主线程共享单例也是不现实的(总有业务限制),所有单例串行执行可能会造成性能问题而且风险很大。目前还没想到可行的方案。

  2. 静态检测工具?

    首先要做静态分析,毕竟之前没做过,门槛太高,性价比低,放弃。

  3. 运行时检测工具?

    想做运行时检测有两件事要做:

    第一件事,在线程申请加锁和解锁once token时,对线程打标记: 

    自己的代码中可以用宏定义改掉dispatch_once的实现,在其中对线程打标记,这个应该不难。

    别人的代码中只能在运行时里面换出sharedInstance, defaultManager等方法来打标记。

    第二件事,找出子线程准备锁主线程的位置: 

    仅可以 hook objective-c 实现的同步方法,不能 hook GCD 的同步方法,所以仍要靠人肉review,而且只能review自己代码,不能review SDK。

    结论是制作此工具可以用作预检,减轻我们部分负担。有时间可以尝试写一下。

  4. 使用的时候人肉多加注意?

    为保证稳定不在dispatch_once中同步执行主线程任务。但是人肉保证难度大。

结论是3和4可以尝试做一下。

相关问题:  The Good, the Bad and the Notification 。