iOS底層 - 關於死鎖,你瞭解多少?

這是我參與8月更文挑戰的第10天,活動詳情查看:8月更文挑戰markdown

寫在前面: iOS底層原理探究是本人在平時的開發和學習中不斷積累的一段進階之
路的。 記錄個人不斷探索之旅,但願能有幫助到各位讀者朋友。
複製代碼

目錄以下:

  1. iOS 底層原理探索 之 alloc
  2. iOS 底層原理探索 之 結構體內存對齊
  3. iOS 底層原理探索 之 對象的本質 & isa的底層實現
  4. iOS 底層原理探索 之 isa - 類的底層原理結構(上)
  5. iOS 底層原理探索 之 isa - 類的底層原理結構(中)
  6. iOS 底層原理探索 之 isa - 類的底層原理結構(下)
  7. iOS 底層原理探索 之 Runtime運行時&方法的本質
  8. iOS 底層原理探索 之 objc_msgSend
  9. iOS 底層原理探索 之 Runtime運行時慢速查找流程
  10. iOS 底層原理探索 之 動態方法決議
  11. iOS 底層原理探索 之 消息轉發流程
  12. iOS 底層原理探索 之 應用程序加載原理dyld (上)
  13. iOS 底層原理探索 之 應用程序加載原理dyld (下)
  14. iOS 底層原理探索 之 類的加載
  15. iOS 底層原理探索 之 分類的加載
  16. iOS 底層原理探索 之 關聯對象
  17. iOS底層原理探索 之 魔法師KVC
  18. iOS底層原理探索 之 KVO原理|8月更文挑戰
  19. iOS底層原理探索 之 重寫KVO|8月更文挑戰
  20. iOS底層原理探索 之 多線程原理|8月更文挑戰
  21. iOS底層原理探索 之 GCD函數和隊列
  22. iOS底層原理探索 之 GCD原理(上)

以上內容的總結專欄


細枝末節整理


前言

咱們從GCD函數和隊列的內容中最後的經典案例中關於死鎖的案例開始,從死鎖的發生開始,看看其產生的本質緣由是爲何。話很少說這就開始。數據結構

引用咱們在GCD函數和隊列一文中和死鎖相關的內容:多線程

  • 建立串行調度隊列的解釋:

當咱們但願任務以特定的順序執行時,串行隊列頗有用。串行隊列一次只執行一個任務,而且老是從隊列的頭部拉去任務。咱們能夠使用串行隊列而不是鎖來保護共享資源或可變數據結構。與鎖不一樣,串行隊列確保任務以能夠預測的順序執行。並且 只要咱們將任務異步提交到串行隊列,隊列就永遠不會死鎖併發

  • 在將單個任務添加到隊列中的解釋:

有兩種方法能夠將任務添加到隊列中:異步或同步。若是可能,使用dispatch_asyncdispatch_async_f函數的異步執行優先於同步替代方案。當您將塊對象或函數添加到隊列時,沒法知道該代碼什麼時候執行。所以,異步添加塊或函數可以讓您安排代碼的執行並繼續從調用線程執行其餘工做。若是您從應用程序的主線程調度任務,這尤爲重要——也許是爲了響應某些用戶事件。異步

儘管您應該儘量以異步方式添加任務,但有時您仍可能須要同步添加任務以防止競爭條件或其餘同步錯誤。在這些狀況下,您可使用dispatch_syncdispatch_sync_f函數將任務添加到隊列中。這些函數會阻塞當前的執行線程,直到指定的任務完成執行。async

重要提示: 您永遠不該從在您計劃傳遞給函數的同一隊列中執行的任務調用dispatch_syncdispatch_sync_f函數。這對於保證死鎖的串行隊列尤爲重要,但對於併發隊列也應避免函數

以上兩部份內容,是在闡述串行隊列的概念解釋和將任務添加到隊列中的兩種方式的規範內容。總的來講,是幫助咱們在正確使用串行隊列,以及在將任務添加到隊列中時,避免死鎖的發生。post

下面,咱們從案例中的死鎖開始。學習

死鎖的發生

正如上面的重要提示中鎖闡述的同樣,咱們永遠不該該將函數添加到隊列中執行任務時使用同步的方式。這對於保證死鎖的串行隊列尤爲重要,但對於併發隊列也應避免。ui

的確,這是避免死鎖的重要思路,可是,仍是難以免,在實際開發中,咱們使用了下面的代碼:

串行隊列,同步函數中同步函數

image.png

第180行,咱們的程序發生來死鎖,從堆棧的信息中能夠看到是 :

libdispatch.dylib_dispatch_sync_f_slow: -> libdispatch.dylib__DISPATCH_WAIT_FOR_QUEUE__:

跟蹤流程

在180行,打上斷點。在GCD函數和隊列篇章中,咱們知道dispatch_syn函數的執行流程以下:

dispatch_sync -> _dispatch_sync_f -> _dispatch_sync_f_inline

在_dispatch_sync_f_inline中,有五條分支,咱們分別下五個符號斷點:

_dispatch_sync_f_inline

static inline void
_dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt, dispatch_function_t func, uintptr_t dc_flags)
{
	if (likely(dq->dq_width == 1)) {
		return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags);
	}

	if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
		DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
	}

	dispatch_lane_t dl = upcast(dq)._dl;
	// 全局併發隊列和綁定到非分派線程的隊列
	// 老是落在慢的狀況下 DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
	if (unlikely(!_dispatch_queue_try_reserve_sync_width(dl))) {
		return _dispatch_sync_f_slow(dl, ctxt, func, 0, dl, dc_flags);
	}

	if (unlikely(dq->do_targetq->do_targetq)) {
		return _dispatch_sync_recurse(dl, ctxt, func, dc_flags);
	}
	_dispatch_introspection_sync_begin(dl);
	_dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG(
			_dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags)));
}
複製代碼

先來到了 _dispatch_barrier_sync_f 分支

image.png

_dispatch_barrier_sync_f

static void
_dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func, uintptr_t dc_flags)
{
_dispatch_barrier_sync_f_inline(dq, ctxt, func, dc_flags);
}
複製代碼

再下 _dispatch_barrier_sync_f_inline 符號斷點, 很遺憾沒有斷住,直接來到了 _dispatch_sync_f_slow :

image.png

咱們來到 _dispatch_barrier_sync_f_inline 內部的實現:

_dispatch_barrier_sync_f_inline

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_barrier_sync_f_inline(dispatch_queue_t dq, void *ctxt, dispatch_function_t func, uintptr_t dc_flags)
{
	dispatch_tid tid = _dispatch_tid_self();

	if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
		DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
	}

	dispatch_lane_t dl = upcast(dq)._dl;
	// 更正確的作法是合併線程的qos
	// 剛剛得到了進入隊列狀態的barrier鎖。
	//
	// 然而,這對於快速路徑來講太昂貴了,因此跳過它。
	// 選擇的權衡是,若是隊列在較低優先級的線程上
	// 與此快速路徑,此線程可能收到無用的覆蓋。
	//
	// 全局併發隊列和綁定到非分派線程的隊列
	// 老是落在慢的狀況下 DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
	if (unlikely(!_dispatch_queue_try_acquire_barrier_sync(dl, tid))) {
		return _dispatch_sync_f_slow(dl, ctxt, func, DC_FLAG_BARRIER, dl,
				DC_FLAG_BARRIER | dc_flags);
	}

	if (unlikely(dl->do_targetq->do_targetq)) {
		return _dispatch_sync_recurse(dl, ctxt, func,
				DC_FLAG_BARRIER | dc_flags);
	}
	_dispatch_introspection_sync_begin(dl);
	_dispatch_lane_barrier_sync_invoke_and_complete(dl, ctxt, func
			DISPATCH_TRACE_ARG(_dispatch_trace_item_sync_push_pop(
					dq, ctxt, func, dc_flags | DC_FLAG_BARRIER)));
}
複製代碼

(_dispatch_sync_f_inline 和 _dispatch_barrier_sync_f_inline 內部實現有點類似)

其內部的第二個分支即是調用 _dispatch_sync_f_slow

_dispatch_sync_f_slow

DISPATCH_NOINLINE
static void
_dispatch_sync_f_slow(dispatch_queue_class_t top_dqu, void *ctxt, dispatch_function_t func, uintptr_t top_dc_flags, dispatch_queue_class_t dqu, uintptr_t dc_flags)
{
	dispatch_queue_t top_dq = top_dqu._dq;
	dispatch_queue_t dq = dqu._dq;
	if (unlikely(!dq->do_targetq)) {
		return _dispatch_sync_function_invoke(dq, ctxt, func);
	}

	pthread_priority_t pp = _dispatch_get_priority();
	struct dispatch_sync_context_s dsc = {
		.dc_flags    = DC_FLAG_SYNC_WAITER | dc_flags,
		.dc_func     = _dispatch_async_and_wait_invoke,
		.dc_ctxt     = &dsc,
		.dc_other    = top_dq,
		.dc_priority = pp | _PTHREAD_PRIORITY_ENFORCE_FLAG,
		.dc_voucher  = _voucher_get(),
		.dsc_func    = func,
		.dsc_ctxt    = ctxt,
		.dsc_waiter  = _dispatch_tid_self(),
	};

	_dispatch_trace_item_push(top_dq, &dsc);
	__DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq);

	if (dsc.dsc_func == NULL) {
		// dsc_func being cleared means that the block ran on another thread ie.
		// case (2) as listed in _dispatch_async_and_wait_f_slow.
		dispatch_queue_t stop_dq = dsc.dc_other;
		return _dispatch_sync_complete_recurse(top_dq, stop_dq, top_dc_flags);
	}

	_dispatch_introspection_sync_begin(top_dq);
	_dispatch_trace_item_pop(top_dq, &dsc);
	_dispatch_sync_invoke_and_complete_recurse(top_dq, ctxt, func,top_dc_flags
			DISPATCH_TRACE_ARG(&dsc));
}
複製代碼

看到其實現內容,依然會以爲和上面兩個(_dispatch_sync_f_inline 和 _dispatch_barrier_sync_f_inline) 有點類似。 再次根據分支下符號斷點,就斷不住了,直接會崩潰,根據堆棧,咱們會來到: __DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq) 的執行。

image.png image.png

最終奔潰的點

__DISPATCH_WAIT_FOR_QUEUE__

DISPATCH_NOINLINE
static void
__DISPATCH_WAIT_FOR_QUEUE__(dispatch_sync_context_t dsc, dispatch_queue_t dq)
{
    uint64_t dq_state = _dispatch_wait_prepare(dq);
    if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
            DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
                            "dispatch_sync called on queue "
                            "already owned by current thread");
    }
    
    ...
    
}
複製代碼

程序崩潰在這裏,那麼,咱們就須要重點分析下這裏的 if 判斷條件是什麼?符合了怎樣的條件,以致於程序崩潰的發生。

函數對比的兩個內容:

// 一、當前的隊列線程的線程ID
#define _dispatch_tid_self() ((dispatch_tid)_dispatch_thread_port())

#define _dispatch_thread_port() pthread_mach_thread_np(_dispatch_thread_self())

#define _dispatch_thread_self() ((uintptr_t)pthread_self())

// 二、隊列的狀態
 _dispatch_wait_prepare(dq);

複製代碼

_dq_state_drain_locked_by

ISPATCH_ALWAYS_INLINE
static inline bool
_dq_state_drain_locked_by(uint64_t dq_state, dispatch_tid tid)
{
	return _dispatch_lock_is_locked_by((dispatch_lock)dq_state, tid);
}

#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc)

DISPATCH_ALWAYS_INLINE
static inline bool
_dispatch_lock_is_locked_by(dispatch_lock lock_value, dispatch_tid tid)
{
	// equivalent to _dispatch_lock_owner(lock_value) == tid
        // DLOCK_OWNER_MASK 是一個很大的數 ((dispatch_lock)0xfffffffc)
        // 前面的結果只要不爲0 與上 DLOCK_OWNER_MASK 也不爲0
        // 若是前面的結果與上DLOCK_OWNER_MASK結果爲0 那前面的結果 必然爲0
        // 最終, lock_value 和 tid 相同 纔會 爲 0
	return ((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0;
}
複製代碼

總結

最後, 原本這個鎖住要等待的線程的狀態和咱們的線程ID相同。也就是咱們的線程原本應該在等待狀態,然而這個時候,又調用了線程的隊列來添加任務,告訴系統要調起此線程,結果在咱們的系統中此線程又是等待的狀態。因此,這次添加任務是沒法實現的。

在這裏,又要調起線程,而後線程又是等待狀態,此時就是一個矛盾,沒法繼續執行下去,因此就發生了死鎖

相關文章
相關標籤/搜索