dispatch_sync死鎖問題研究

首先,看看如下代碼的輸出是什么?

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"Hello");
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"World");
            });
}

首先答案是會發生死鎖,我們看看官方文檔關于dispatch_sync的解釋:

Submits a block to a dispatch queue like dispatch_async(), however
dispatch_sync() will not return until the block has finished.

Calls to dispatch_sync() targeting the current queue will result
in dead-lock. Use of dispatch_sync() is also subject to the same
multi-party dead-lock problems that may result from the use of a mutex
.
Use of dispatch_async() is preferred.

Unlike dispatch_async(), no retain is performed on the target queue. Because
calls to this function are synchronous, the dispatch_sync() "borrows" the
reference of the caller.

As an optimization, dispatch_sync() invokes the block on the current
thread when possible.

如果dispatch_sync()的目標queue為當前queue,會發生死鎖(并行queue并不會)。使用dispatch_sync()會遇到跟我們在pthread中使用mutex鎖一樣的死鎖問題。

話是這么說,我們看看究竟是怎么做的?先放碼:

source/queue.c

void
dispatch_sync(dispatch_queue_t dq, void (^work)(void))
{
    struct Block_basic *bb = (void *)work;
    dispatch_sync_f(dq, work, (dispatch_function_t)bb->Block_invoke);
}

DISPATCH_NOINLINE
void
dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func)
{
    typeof(dq->dq_running) prev_cnt;
    dispatch_queue_t old_dq;

    if (dq->dq_width == 1) {
        return dispatch_barrier_sync_f(dq, ctxt, func);
    }

    // 1) ensure that this thread hasn't enqueued anything ahead of this call
    // 2) the queue is not suspended
    if (slowpath(dq->dq_items_tail) || slowpath(DISPATCH_OBJECT_SUSPENDED(dq))) {
        _dispatch_sync_f_slow(dq);
    } else {
        prev_cnt = dispatch_atomic_add(&dq->dq_running, 2) - 2;

        if (slowpath(prev_cnt & 1)) {
            if (dispatch_atomic_sub(&dq->dq_running, 2) == 0) {
                _dispatch_wakeup(dq);
            }
            _dispatch_sync_f_slow(dq);
        }
    }

    old_dq = _dispatch_thread_getspecific(dispatch_queue_key);
    _dispatch_thread_setspecific(dispatch_queue_key, dq);
    func(ctxt);
    _dispatch_workitem_inc();
    _dispatch_thread_setspecific(dispatch_queue_key, old_dq);

    if (slowpath(dispatch_atomic_sub(&dq->dq_running, 2) == 0)) {
        _dispatch_wakeup(dq);
    }
}

Step1. 可以看到dispatch_sync將我們block函數指針進行了一些轉換后,直接傳給了dispatch_sync_f()去處理。

Step2. dispatch_sync_f首先檢查傳入的隊列寬度(dq_width),由于我們傳入的main queue為串行隊列,隊列寬度為1,所有接下來會調用dispatch_barrier_sync_f,傳入3個參數,dispatch_sync中的目標queue、上下文信息和由我們block函數指針轉化過后的func結構體。

接下來我們看看dispatch_barrier_sync_f的實現

source/queue.c

void
dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func)
{
    dispatch_queue_t old_dq = _dispatch_thread_getspecific(dispatch_queue_key);

    // 1) ensure that this thread hasn't enqueued anything ahead of this call
    // 2) the queue is not suspended
    // 3) the queue is not weird
    if (slowpath(dq->dq_items_tail)
            || slowpath(DISPATCH_OBJECT_SUSPENDED(dq))
            || slowpath(!_dispatch_queue_trylock(dq))) {
        return _dispatch_barrier_sync_f_slow(dq, ctxt, func);
    }

    _dispatch_thread_setspecific(dispatch_queue_key, dq);
    func(ctxt);
    _dispatch_workitem_inc();
    _dispatch_thread_setspecific(dispatch_queue_key, old_dq);
    _dispatch_queue_unlock(dq);
}

Step3. disptach_barrier_sync_f首先做了做了3個判斷:

  • 隊列存在尾部節點狀態(判斷當前是不是處于隊列尾部)
  • 隊列不為暫停狀態
  • 使用_dispatch_queue_trylock檢查隊列能被正常加鎖。

滿足所有條件則不執行if語句內的內容,執行下面代碼,簡單解釋為:

  • 使用mutex鎖,獲取到當前進程資源鎖。
  • 直接執行我們block函數指針的具體內容。
  • 然后釋放鎖,整個調用結束。

然后在我們例子中,很顯然當前隊列中還有其他viewController的任務,我們的流程跑到_dispatch_barrier_aync_f_slow()函數體中。

刨根問底,讓我們看看這個函數。

source/queue.c

static void
_dispatch_barrier_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func)
{
    
    // It's preferred to execute synchronous blocks on the current thread
    // due to thread-local side effects, garbage collection, etc. However,
    // blocks submitted to the main thread MUST be run on the main thread
    
    struct dispatch_barrier_sync_slow2_s dbss2 = {
        .dbss2_dq = dq,
#if DISPATCH_COCOA_COMPAT
        .dbss2_func = func,
        .dbss2_ctxt = ctxt,
#endif
        .dbss2_sema = _dispatch_get_thread_semaphore(),
    };
    struct dispatch_barrier_sync_slow_s {
        DISPATCH_CONTINUATION_HEADER(dispatch_barrier_sync_slow_s);
    } dbss = {
        .do_vtable = (void *)DISPATCH_OBJ_BARRIER_BIT,
        .dc_func = _dispatch_barrier_sync_f_slow_invoke,
        .dc_ctxt = &dbss2,
    };
//---------------重點是這里---------------   
    _dispatch_queue_push(dq, (void *)&dbss);
    dispatch_semaphore_wait(dbss2.dbss2_sema, DISPATCH_TIME_FOREVER);
    _dispatch_put_thread_semaphore(dbss2.dbss2_sema);

#if DISPATCH_COCOA_COMPAT
    // Main queue bound to main thread
    if (dbss2.dbss2_func == NULL) {
        return;
    }
#endif
    dispatch_queue_t old_dq = _dispatch_thread_getspecific(dispatch_queue_key);
    _dispatch_thread_setspecific(dispatch_queue_key, dq);
    func(ctxt);
    _dispatch_workitem_inc();
    _dispatch_thread_setspecific(dispatch_queue_key, old_dq);
    dispatch_resume(dq);
}

Step4. 既然我們上面已經判斷了,main queue中還有其他任務,現在不能直接執行這個block,跳入到_dispatch_barrier_sync_f_slow函數體,那它怎么處理我們加入的block呢?

在_dispatch_barrier_sync_f_slow中,使用_dispatch_queue_push將我們的block壓入main queue的FIFO隊列中,然后等待信號量,ready后被喚醒。

然后dispatch_semaphore_wait返回_dispatch_semaphore_wait_slow(dsema, timeout)函數,持續輪訓并等待,直到條件滿足。

所以在此過程中,我們最初調用的dispatch_sync函數一直得不到返回,main queue被阻塞,而我們的block又需要等待main queue來執行它。死鎖愉快的產生了。

最后:

我們繪制上張圖來輕松的描述一下這個問題:

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

推薦閱讀更多精彩內容

  • 首先,看看如下代碼的輸出是什么? - (void)viewDidLoad { [superviewDidLoad...
    made_China閱讀 872評論 0 2
  • 簡介 GCD(Grand Central Dispatch)是在macOS10.6提出來的,后來在iOS4.0被引...
    sunmumu1222閱讀 882評論 0 2
  • 講清這個問題需要理解以下幾個基本知識 線程是什么? GCD中隊列與任務是什么,sync和async方法是什么樣的機...
    minking1982閱讀 1,241評論 1 4
  • 一. 重點: 1.dispatch_queue_create(生成Dispatch Queue) 2.Main D...
    BestJoker閱讀 1,593評論 2 2
  • 人生的枷鎖就是追求百分之百的安全感。 當我們追求百分之百安全感的時候,我們沒有辦法集中精力去深入地思考,長期地思考...
    WindyLiu閱讀 635評論 4 3