iOS開發者在與線程打交道的方式中,使用最多的應該就是GCD框架了,沒有之一。GCD將繁瑣的線程抽象爲了一個個隊列,讓開發者極易理解和使用。但其實隊列的底層,依然是利用線程實現的,一樣會有死鎖的問題。本文將探討如何規避disptach_sync
接口引入的死鎖問題。安全
GCD最基礎的兩個接口markdown
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
disptach_async
是異步扔一個block
到queue
中,即扔完我就無論了,繼續執行個人下一行代碼。實際上當下一行代碼執行時,這個block
還未執行,只是入了隊列queue
,queue
會排隊來執行這個block
。post
而disptach_sync
則是同步扔一個block
到queue
中,即扔了我就等着,等到queue
排隊把這個block
執行完了以後,才繼續執行下一行代碼。測試
disptach_sync
主要用於代碼上下文對時序有強要求的場景。簡單點說,就是下一行代碼的執行,依賴於上一行代碼的結果。例如說,咱們須要在子線程中讀取一個image
對象,使用接口[UIImage imageNamed:]
,但imageNamed:
實際上在iOS9之後纔是線程安全的,iOS9以前都須要在主線程獲取。因此,咱們須要從子線程切換到主線程獲取image
,而後再切回子線程拿到這個image
,spa
// ...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
...
要定位死鎖的問題,咱們須要知道在哪一行代碼上死鎖了,以及爲何會出現死鎖。一般只要知道哪一行代碼死鎖了,咱們就能經過代碼分析出問題所在了。因此,若是死鎖的時候,咱們可以把堆棧上報上來,就能知道哪一行代碼死鎖了。這裏須要有完善的死鎖監測和堆棧上報機制。
若是暫時沒有人力或者技術支撐你去搭建完善的死鎖監測和堆棧上報機制,那麼你能夠作一件簡單的事情以協助你定位問題,那就是打印日誌。在dispatch_sync
或者加鎖以前,打印一條日誌。這樣在用戶反饋問題,或者測試重現問題的時候,提取日誌即可分析出卡死的代碼處。
答案是,儘可能不要使用。沒有哪個接口是能夠保證絕對安全的。必需要使用dispatch_sync
的時候,儘可能使用dispatch_sync_on_main_queue
這個API。
如有發現問題,或是更好的建議,歡迎私信或者評論:)