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

现象 最近遇到了一个死锁crash,主线程在dispatch_once时卡住了: Thread 0 name: Dispatch queue: com

现象

最近遇到了一个死锁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

未登录用户
全部评论0
到底啦