现象
最近遇到了一个死锁crash,主线程在dispatch_once时卡住了:
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 __ulock_wait + 8
1 _dispatch_unfair_lock_wait + 48
2 _dispatch_gate_wait_slow + 56
3 dispatch_once_f + 124
4 +[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 + 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 -[OTReachability networkStatusForFlags:] (AFReachability.m:216)
...
25 start_wqthread + 4
Thread 4:
0 __semwait_signal + 8
1 nanosleep + 212
2 usleep + 64
3 wpthread_main + 216
4 _pthread_body + 240
5 _pthread_body + 0
6 thread_start + 4
Thread 7 name: Dispatch queue: com.apple.root.default-qos
Thread 7:
0 semaphore_wait_trap + 8
1 _dispatch_semaphore_wait_slow + 216
2 CFURLConnectionSendSynchronousRequest + 284
3 +[NSURLConnection sendSynchronousRequest:returningResponse:error:] + 120
4 +[UTMCHttpHelper post:url:dict:len:errorCode:] + 1864
5 __39-[UTMCOnlineConfManager syncOnlineconf]_block_invoke + 660
6 _dispatch_call_block_and_release + 24
7 _dispatch_client_callout + 16
8 _dispatch_queue_override_invoke + 732
9 _dispatch_root_queue_drain + 572
10 _dispatch_worker_thread3 + 124
11 _pthread_wqthread + 1288
12 start_wqthread + 4
Thread 18 name: Dispatch queue: com.apple.NSURLSession-work
Thread 18:
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)
...
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上如果不是发了一个阻塞式的通知,而是做了一些其他的需要在主线程执行并同步返回的事,也会造成死锁。
解决方案
-
自动解决或加保护?
如果围绕自动解决或者加保护的方式来做,禁止子线程同步调用主线程也好是不现实的(总有业务限制),禁止子线程和主线程共享单例也是不现实的(总有业务限制),所有单例串行执行可能会造成性能问题而且风险很大。目前还没想到可行的方案。
-
静态检测工具?
首先要做静态分析,毕竟之前没做过,门槛太高,性价比低,放弃。
-
运行时检测工具?
想做运行时检测有两件事要做:
第一件事,在线程申请加锁和解锁once token时,对线程打标记:
自己的代码中可以用宏定义改掉dispatch_once的实现,在其中对线程打标记,这个应该不难。
别人的代码中只能在运行时里面换出sharedInstance, defaultManager等方法来打标记。
第二件事,找出子线程准备锁主线程的位置:
仅可以 hook objective-c 实现的同步方法,不能 hook GCD 的同步方法,所以仍要靠人肉review,而且只能review自己代码,不能review SDK。
结论是制作此工具可以用作预检,减轻我们部分负担。有时间可以尝试写一下。
-
使用的时候人肉多加注意?
为保证稳定不在dispatch_once中同步执行主线程任务。但是人肉保证难度大。
结论是3和4可以尝试做一下。