概述
iOS開發者在與線程打交道的方式中,使用最多的應該就是GCD框架了,沒有之一。GCD將繁瑣的線程抽象為了一個個隊列,讓開發者極易理解和使用。但其實隊列的底層,依然是利用線程實現的,同樣會有死鎖的問題。本文將探討如何規避disptach_sync
接口引入的死鎖問題。
GCD基礎
GCD最基礎的兩個接口
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
第一個參數queue
為隊列對象,第二個參數block
為block對象。這兩個接口可以將任務block
扔到隊列queue
中去執行。
開發者使用最頻繁的,就是在子線程環境下,需要做UI更新時,我們可以將任務扔到主線程去執行,
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
中,即扔完我就不管了,繼續執行我的下一行代碼。實際上當下一行代碼執行時,這個block
還未執行,只是入了隊列queue
,queue
會排隊來執行這個block
。
而disptach_sync
則是同步扔一個block
到queue
中,即扔了我就等著,等到queue
排隊把這個block
執行完了之后,才繼續執行下一行代碼。
為什么要使用sync
disptach_sync
主要用于代碼上下文對時序有強要求的場景。簡單點說,就是下一行代碼的執行,依賴于上一行代碼的結果。例如說,我們需要在子線程中讀取一個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
。
為什么會死鎖
假設當前我們的代碼正在queue0
中執行。然后我們調用disptach_sync
將一個任務block1
扔到queue0
中執行,
// ... currently in queue0 or queue0's corresponding thread.
dispatch_sync(queue0, block1);
這時,dispatch_sync
將等待queue0
排隊執行完block1
,然后才能繼續執行下一行代碼。But,當前代碼執行的環境也是queue0
。假設當前執行的任務為block0
。也就是說,block0
在執行到一半時,需要等到自己的下一個任務block1
執行完,自己才能繼續執行。而block1
排隊在后面,需要等block0
執行完才能執行。這時死鎖就產生了,block0
和block1
互相等待執行,當前線程就卡死在dispatch_sync
這行代碼處。
我們發現的卡死問題,一般都是主線程死鎖。一種較為常見的情況是,本身就已經在主線程了,還同步向主線程扔了一個任務:
// ... currently in the main thread
dispatch_sync(dispatch_get_main_queue(), block);
安全方法
YYKit中提供了一個同步扔任務到主線程的安全方法:
/**
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);
}
}
其方式就是在扔任務給主線程之前,先檢查當前線程是否已經是主線程,如果是,就不用調用GCD的隊列調度接口dispatch_sync
了,直接執行即可;如果不是主線程,那么調用GCD的dispatch_sync
也不會卡死。
但事實上并不是這樣的,dispatch_sync_on_main_queue
也可能會卡死,這個安全接口并不安全。這個接口只能保證兩個block
之間不因互相等待而死鎖。多于兩個block
的互相依賴就束手無策了。
舉個例子,假設queue0
是一個子線程的隊列:
/* block0 */
// ... currently in the main thread.
dispatch_sync(queue0, ^{
/* block1 */
// ... currently in queue0's corresponding subthread.
dispatch_sync_on_main_queue(^{
/* block2 */
});
});
在上述代碼中,block0
正在主線程中執行,并且同步等待子線程執行完block1
。block1
又同步等待主線程執行完block2
。而當前主線程正在執行block0
,即block2
的執行需要等到block0
執行完。這樣就成了block0
-->block1
-->block2
-->block0
...這樣一個循環等待,即死鎖。由于block1
的環境是子線程,所以安全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
的執行是同步的,這里會出現和上一例一樣的死鎖情況:block0
-->block1
-->block2
-->block0
...
如何定位死鎖問題
1.死鎖監測和堆棧上報機制
要定位死鎖的問題,我們需要知道在哪一行代碼上死鎖了,以及為什么會出現死鎖。通常只要知道哪一行代碼死鎖了,我們就能通過代碼分析出問題所在了。所以,如果死鎖的時候,我們能夠把堆棧上報上來,就能知道哪一行代碼死鎖了。這里需要有完善的死鎖監測和堆棧上報機制。
2.打印日志
如果暫時沒有人力或者技術支撐你去搭建完善的死鎖監測和堆棧上報機制,那么你可以做一件簡單的事情以協助你定位問題,那就是打印日志。在dispatch_sync
或者加鎖之前,打印一條日志。這樣在用戶反饋問題,或者測試重現問題的時候,提取日志便可分析出卡死的代碼處。
如何安全使用dispatch_sync
答案是,盡量不要使用。沒有哪一個接口是可以保證絕對安全的。必須要使用dispatch_sync
的時候,盡量使用dispatch_sync_on_main_queue
這個API。
若有發現問題,或是更好的建議,歡迎私信或者評論:)