如何安全使用dispatch_sync

概述

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更新時,咱們能夠將任務扔到主線程去執行,async

dispatch_sync(dispatch_get_main_queue(), block); dispatch_async(dispatch_get_main_queue(), block); 

dispatch_sync(dispatch_get_main_queue(), block)有可能引入死鎖的問題。post

async VS.sync

disptach_async是異步扔一個blockqueue中,即扔完我就無論了,繼續執行個人下一行代碼。實際上當下一行代碼執行時,這個block還未執行,只是入了隊列queuequeue會排隊來執行這個block測試

disptach_sync則是同步扔一個blockqueue中,即扔了我就等着,等到queue排隊把這個block執行完了以後,才繼續執行下一行代碼。ui


爲何要使用sync

disptach_sync主要用於代碼上下文對時序有強要求的場景。簡單點說,就是下一行代碼的執行,依賴於上一行代碼的結果。例如說,咱們須要在子線程中讀取一個image對象,使用接口[UIImage imageNamed:],但imageNamed:實際上在iOS9之後纔是線程安全的,iOS9以前都須要在主線程獲取。因此,咱們須要從子線程切換到主線程獲取image,而後再切回子線程拿到這個imagespa

// ...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執行完才能執行。這時死鎖就產生了,block0block1互相等待執行,當前線程就卡死在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正在主線程中執行,而且同步等待子線程執行完block1block1又同步等待主線程執行完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。

做者:Joey_Xu 連接:https://www.jianshu.com/p/b3227582037d 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索