如何安全使用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更新時,我們可以將任務扔到主線程去執行,

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是異步扔一個blockqueue中,即扔完我就不管了,繼續執行我的下一行代碼。實際上當下一行代碼執行時,這個block還未執行,只是入了隊列queuequeue會排隊來執行這個block

disptach_sync則是同步扔一個blockqueue中,即扔了我就等著,等到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執行完才能執行。這時死鎖就產生了,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。


若有發現問題,或是更好的建議,歡迎私信或者評論:)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容