一個(gè)dispatch_once死鎖問(wèn)題
工作中遇到一問(wèn)題:
我們有一個(gè)埋點(diǎn)SDK,
啟動(dòng)app,SDK的單例類(lèi)的初始在主線程執(zhí)行 [[Sdk shareInstance] startWithConfig]
同時(shí)有一個(gè)業(yè)務(wù)方法在子線程調(diào)用該sdk的方法去埋點(diǎn),埋點(diǎn)方法也是調(diào)
[[Sdk shareInstance] addEvent:];
在[Sdk shareInstance]的方法里會(huì)出現(xiàn)一定概率的死鎖!!
先看看模擬代碼
通常我們用dispatch_once寫(xiě)單例如下:
注意一下,我們?cè)趇nit方法里面調(diào)用到一個(gè)切到主線程的方法ksk_syncRunOnMainQueue, 在主線程中做了點(diǎn)事情。
#import "SSTestManager.h"
NS_INLINE void ksk_syncRunOnMainQueue(void (^block)(void)) {
if (!block) return;
if ([NSThread isMainThread]) {
block();
} else {
dispatch_sync(dispatch_get_main_queue(), block);
}
}
@implementation SSTestManager
+ (instancetype)sharedInstance {
static SSTestManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self.class alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
[self observeAppLife];
}
return self;
}
- (void)observeAppLife {
ksk_syncRunOnMainQueue(^{
// do something on main thread
NSLog(@"test");
});
}
@end
調(diào)用的地方,在主線程中
@implementation QTGCDIndexController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 當(dāng)前主線程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[SSTestManager sharedInstance];
});
// 讓主線程休息1s
// sleep(1);
[SSTestManager sharedInstance];
}
@end
模擬重現(xiàn)步驟:
- 正常執(zhí)行,沒(méi)有問(wèn)題。執(zhí)行順序是先主線中調(diào)用[SSTestManager sharedInstance], 然后子線程中執(zhí)行
- 放開(kāi)sleep(1), 讓子線程先執(zhí)行起來(lái)[SSTestManager sharedInstance],主線程再執(zhí)行,發(fā)現(xiàn)死鎖了,為什么???思考思考,...
問(wèn)題分析:
- 正常執(zhí)行,主線程完成sharedInstance之后,子線程再開(kāi)始執(zhí)行,所以子線程同步向主線提交任務(wù),dispatch_sync(dispatch_get_main_queue(), block),主線程沒(méi)有等待任務(wù),直接在主線程執(zhí)行。
- 放開(kāi)sleep之后,子線程在執(zhí)行sharedInstance中,調(diào)用dispatch_sync(dispatch_get_main_queue(), block)同步向主線程提交任務(wù),它要等待主線程任務(wù)處理完,再執(zhí)行當(dāng)前block,而同時(shí)主線程執(zhí)行sharedInstance方法,由于dispatch_once正在被子線程鎖,此時(shí)主線程block等待子線程完成。這樣,就造成了互相等待,死鎖發(fā)生了。。
結(jié)論:
- dispatch_once中的代碼盡量只做初始化的事情,不要調(diào)用很多其他的方法。
- dispatch_once盡量不要切換線程,特別是同步提交到其他線程的任務(wù)。
- dispatch_once中的代碼盡量不要拋出異常,不要Crash。
- 盡量能保持自給自足,減少對(duì)別的模塊或者類(lèi)的依賴(lài);
dispatch_once死鎖方式
死鎖方式1:
1、某線程T1()調(diào)用單例A,且為應(yīng)用生命周期內(nèi)首次調(diào)用,需要使用dispatch_once(&token, block())初始化單例。
2、上述block()中的某個(gè)函數(shù)調(diào)用了dispatch_sync_safe,同步在T2線程執(zhí)行代碼
3、T2線程正在執(zhí)行的某個(gè)函數(shù)需要調(diào)用到單例A,將會(huì)再次調(diào)用dispatch_once。
4、這樣T1線程在等block執(zhí)行完畢,它在等待T2線程執(zhí)行完畢,而T2線程在等待T1線程的dispatch_once執(zhí)行完畢,造成了相互等待,故而死鎖
死鎖方式2:
1、某線程T1()調(diào)用單例A,且為應(yīng)用生命周期內(nèi)首次調(diào)用,需要使用dispatch_once(&token, block())初始化單例;
2、block中可能掉用到了B流程,B流程又調(diào)用了C流程,C流程可能調(diào)用到了單例A,將會(huì)再次調(diào)用dispatch_once;
3、這樣又造成了相互等待。