概述
iOS開發(fā)者在與線程打交道的方式中族沃,使用最多的應(yīng)該就是GCD框架了频祝,沒有之一泌参。GCD將繁瑣的線程抽象為了一個個隊列,讓開發(fā)者極易理解和使用常空。但其實隊列的底層沽一,依然是利用線程實現(xiàn)的,同樣會有死鎖的問題漓糙。本文將探討如何規(guī)避disptach_sync
接口引入的死鎖問題铣缠。
GCD基礎(chǔ)
GCD最基礎(chǔ)的兩個接口
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
第一個參數(shù)queue
為隊列對象,第二個參數(shù)block
為block對象昆禽。這兩個接口可以將任務(wù)block
扔到隊列queue
中去執(zhí)行蝗蛙。
開發(fā)者使用最頻繁的,就是在子線程環(huán)境下醉鳖,需要做UI更新時捡硅,我們可以將任務(wù)扔到主線程去執(zhí)行,
dispatch_sync(dispatch_get_main_queue(), block);
dispatch_async(dispatch_get_main_queue(), block);
而dispatch_sync(dispatch_get_main_queue(), block)
有可能引入死鎖的問題盗棵。
async VS. sync
disptach_async
是異步扔一個block
到queue
中壮韭,即扔完我就不管了,繼續(xù)執(zhí)行我的下一行代碼纹因。實際上當(dāng)下一行代碼執(zhí)行時喷屋,這個block
還未執(zhí)行,只是入了隊列queue
瞭恰,queue
會排隊來執(zhí)行這個block
屯曹。
而disptach_sync
則是同步扔一個block
到queue
中,即扔了我就等著惊畏,等到queue
排隊把這個block
執(zhí)行完了之后是牢,才繼續(xù)執(zhí)行下一行代碼。
為什么要使用sync
disptach_sync
主要用于代碼上下文對時序有強要求的場景陕截。簡單點說驳棱,就是下一行代碼的執(zhí)行,依賴于上一行代碼的結(jié)果农曲。例如說社搅,我們需要在子線程中讀取一個image
對象,使用接口[UIImage imageNamed:]
乳规,但imageNamed:
實際上在iOS9以后才是線程安全的形葬,iOS9之前都需要在主線程獲取。所以暮的,我們需要從子線程切換到主線程獲取image
笙以,然后再切回子線程拿到這個image
,
// ...currently in a subthread
__block UIImage *image;
dispatch_sync_on_main_queue(^{
image = [UIImage imageNamed:@"Resource/img"];
});
attachment.image = image;
這里我們必須使用sync
冻辩。
為什么會死鎖
假設(shè)當(dāng)前我們的代碼正在queue0
中執(zhí)行猖腕。然后我們調(diào)用disptach_sync
將一個任務(wù)block1
扔到queue0
中執(zhí)行拆祈,
// ... currently in queue0 or queue0's corresponding thread.
dispatch_sync(queue0, block1);
這時,dispatch_sync
將等待queue0
排隊執(zhí)行完block1
倘感,然后才能繼續(xù)執(zhí)行下一行代碼放坏。But,當(dāng)前代碼執(zhí)行的環(huán)境也是queue0
老玛。假設(shè)當(dāng)前執(zhí)行的任務(wù)為block0
淤年。也就是說,block0
在執(zhí)行到一半時蜡豹,需要等到自己的下一個任務(wù)block1
執(zhí)行完麸粮,自己才能繼續(xù)執(zhí)行。而block1
排隊在后面镜廉,需要等block0
執(zhí)行完才能執(zhí)行豹休。這時死鎖就產(chǎn)生了,block0
和block1
互相等待執(zhí)行桨吊,當(dāng)前線程就卡死在dispatch_sync
這行代碼處威根。
我們發(fā)現(xiàn)的卡死問題,一般都是主線程死鎖视乐。一種較為常見的情況是洛搀,本身就已經(jīng)在主線程了,還同步向主線程扔了一個任務(wù):
// ... currently in the main thread
dispatch_sync(dispatch_get_main_queue(), block);
安全方法
YYKit中提供了一個同步扔任務(wù)到主線程的安全方法:
/**
Submits a block for execution on a main queue and waits until the block completes.
*/
static inline void dispatch_sync_on_main_queue(void (^block)()) {
if (pthread_main_np()) {
block();
} else {
dispatch_sync(dispatch_get_main_queue(), block);
}
}
其方式就是在扔任務(wù)給主線程之前佑淀,先檢查當(dāng)前線程是否已經(jīng)是主線程留美,如果是,就不用調(diào)用GCD的隊列調(diào)度接口dispatch_sync
了伸刃,直接執(zhí)行即可谎砾;如果不是主線程,那么調(diào)用GCD的dispatch_sync
也不會卡死捧颅。
但事實上并不是這樣的景图,dispatch_sync_on_main_queue
也可能會卡死,這個安全接口并不安全碉哑。這個接口只能保證兩個block
之間不因互相等待而死鎖挚币。多于兩個block
的互相依賴就束手無策了。
舉個例子扣典,假設(shè)queue0
是一個子線程的隊列:
/* block0 */
// ... currently in the main thread.
dispatch_sync(queue0, ^{
/* block1 */
// ... currently in queue0's corresponding subthread.
dispatch_sync_on_main_queue(^{
/* block2 */
});
});
在上述代碼中妆毕,block0
正在主線程中執(zhí)行,并且同步等待子線程執(zhí)行完block1
贮尖。block1
又同步等待主線程執(zhí)行完block2
笛粘。而當(dāng)前主線程正在執(zhí)行block0
,即block2
的執(zhí)行需要等到block0
執(zhí)行完。這樣就成了block0
-->block1
-->block2
-->block0
...這樣一個循環(huán)等待薪前,即死鎖润努。由于block1
的環(huán)境是子線程,所以安全API的線程判斷不起任何作用序六。
另舉一個例子:
/* block0 */
// ... currently in the main thread.
[[NSNotificationCenter defaultCenter] postNotificationName:@"aNotification" object:nil];
// ... in another context
[[NSNotificationCenter defaultCenter] addObserverForName:@"aNotification"
object:nil
queue:queue0
usingBlock:^(NSNotification * _Nonnull note) {
/* block1 */
// ... currently in queue0's corresponding subthread.
dispatch_sync_on_main_queue(^{
/* block2 */
});
}];
由于通知NSNotification
的執(zhí)行是同步的,這里會出現(xiàn)和上一例一樣的死鎖情況:block0
-->block1
-->block2
-->block0
...
如何定位死鎖問題
1.死鎖監(jiān)測和堆棧上報機制
要定位死鎖的問題蚤吹,我們需要知道在哪一行代碼上死鎖了例诀,以及為什么會出現(xiàn)死鎖。通常只要知道哪一行代碼死鎖了裁着,我們就能通過代碼分析出問題所在了繁涂。所以,如果死鎖的時候二驰,我們能夠把堆棧上報上來扔罪,就能知道哪一行代碼死鎖了。這里需要有完善的死鎖監(jiān)測和堆棧上報機制桶雀。
2.打印日志
如果暫時沒有人力或者技術(shù)支撐你去搭建完善的死鎖監(jiān)測和堆棧上報機制矿酵,那么你可以做一件簡單的事情以協(xié)助你定位問題,那就是打印日志矗积。在dispatch_sync
或者加鎖之前全肮,打印一條日志。這樣在用戶反饋問題棘捣,或者測試重現(xiàn)問題的時候辜腺,提取日志便可分析出卡死的代碼處。
如何安全使用dispatch_sync
答案是乍恐,盡量不要使用评疗。沒有哪一個接口是可以保證絕對安全的。必須要使用dispatch_sync
的時候茵烈,盡量使用dispatch_sync_on_main_queue
這個API百匆。
若有發(fā)現(xiàn)問題,或是更好的建議呜投,歡迎私信或者評論:)