18-探究iOS底層原理|多執行緒技術【GCD原始碼分析1:dispatch_get_global_queue與dispatch_(a)sync、單例、執行緒死鎖】

語言: CN / TW / HK

前言

之前,我們在探索動畫及渲染相關原理的時候,我們輸出了幾篇文章,解答了iOS動畫是如何渲染,特效是如何工作的疑惑。我們深感系統設計者在創作這些系統框架的時候,是如此腦洞大開,也 深深意識到了解一門技術的底層原理對於從事該方面工作的重要性。

因此我們決定 進一步探究iOS底層原理的任務。繼上一篇文章對GCD主佇列序列佇列&&並行佇列全域性併發佇列探索之後,本篇文章將繼續對GCD多執行緒底層原理的探索

一、 dispatch_get_global_queue全域性併發佇列+dispatch_sync同步函式

dq->dq_width == 1為序列佇列,那麼併發佇列該怎麼走呢? 如下圖,走的是下面的框框中流程 _dispatch_sync_f_inline 但是這麼多的分支,到底是走的哪一個呢?通過對_dispatch_sync_f_slow_dispatch_sync_recurse_dispatch_introspection_sync_begin_dispatch_sync_invoke_and_complete方法下符號斷點,進行跟蹤除錯。

  • 符號斷點除錯

符號斷點除錯 通過下符號斷點跟蹤,發現走了_dispatch_sync_f_slow,如下圖所示:

斷點在_dispatch_sync_f_slow處 通過閱讀原始碼,發現一個有意思的事情,就是_dispatch_sync_invoke_and_complete方法

_dispatch_sync_invoke_and_complete

  • _dispatch_sync_invoke_and_complete

_dispatch_sync_invoke_and_complete

在這個_dispatch_sync_invoke_and_complete方法的第三個引數是func也是需要執行的任務,但是 func的後面的整體也是一個引數,也就是DISPATCH_TRACE_ARG( _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags)) 整體為一個引數,這就有意思了,中間居然沒有逗號分隔開。老鐵,你這很特別啊!夠長的啊!

那麼去DISPATCH_TRACE_ARG定義看看 DISPATCH_TRACE_ARGDISPATCH_TRACE_ARG的巨集定義裡面,你們有沒有發現,這裡居然把逗號放在了裡面,好傢伙,巨集定義裡面還可以這麼玩,蘋果工程師還真有意思哈! DISPATCH_TRACE_ARG 通過全域性的搜尋,發現這個巨集定義有兩處,一個有逗號,一個沒有逗號,這就是根據不同的條件,進行設定,相當於是一個可選的引數,這一波操作又是非常的細節了!

既然下符號斷點會走_dispatch_sync_f_slow方法,現在就去看看這個方法

  • _dispatch_sync_f_slow

_dispatch_sync_f_slow 這裡又是很多的分支,又通過下符號斷點,發現走的是_dispatch_sync_function_invoke方法裡面

  • _dispatch_sync_function_invoke

static void _dispatch_sync_function_invoke(dispatch_queue_class_t dq, void *ctxt, dispatch_function_t func) { _dispatch_sync_function_invoke_inline(dq, ctxt, func); }

  • _dispatch_sync_function_invoke_inline

static inline void _dispatch_sync_function_invoke_inline(dispatch_queue_class_t dq, void *ctxt, dispatch_function_t func) { dispatch_thread_frame_s dtf; _dispatch_thread_frame_push(&dtf, dq); _dispatch_client_callout(ctxt, func); _dispatch_perfmon_workitem_inc(); _dispatch_thread_frame_pop(&dtf); }

  • push 之後呼叫callout執行,最後再 pop,所以可以同步的執行任務

二、 dispatch_async非同步函式

dispatch_async非同步函式的任務,是包裝在 qos裡面的,那麼現在跟蹤流程,去看看

  • dispatch_async

dispatch_async

  • _dispatch_continuation_async

_dispatch_continuation_async

  • dx_push

dx_push 搜尋dx_push呼叫的地方 在這裡插入圖片描述 這裡就先去看看併發佇列裡面的dq_push吧,

  • _dispatch_lane_concurrent_push

_dispatch_lane_concurrent_push 這裡if裡面有對柵欄函式(_dispatch_object_is_barrier)的判斷,柵欄函式這裡就不分析了,後續的部落格裡面會分析的。

_dispatch_lane_concurrent_push裡面會去呼叫_dispatch_lane_push方法,在上面搜尋dx_push的圖裡面,可以看到,在序列佇列裡面是直接呼叫了_dispatch_lane_push,也就是說序列併發都會走這個方法。

  • _dispatch_lane_push

_dispatch_lane_push 最後去呼叫dx_wakeup,再去搜索看看 dx_wakeup dx_wakeup 是一個巨集定義,看看dq_wakeup哪裡呼叫了 dx_wakeup呼叫地方 如上圖可以發現,序列和併發都是_dispatch_lane_wakeup,全域性的是_dispatch_root_queue_wakeup _dispatch_lane_wakeup

  • _dispatch_queue_wakeup

_dispatch_queue_wakeup

通過下符號斷點會走_dispatch_lane_class_barrier_complete _dispatch_lane_class_barrier_complete _dispatch_lane_class_barrier_complete裡面迴圈遞迴一些操作,還看到了一個系統的函式os_atomic_rmw_loop2o,在這個方法裡面要麼返回dx_wakeup或者做其他的一些處理。

併發佇列資訊 _dispatch_lane_concurrent_push _dispatch_continuation_redirect_push

```

define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)

```

通過跟流程和下符號斷點,會走全域性併發佇列的_dispatch_root_queue_push方法。通過下符號斷點,跟蹤原始碼,最終定位到一個重要的方法_dispatch_root_queue_poke_slow

dispatch_root_queue_push_inline(dispatch_queue_global_t dq, dispatch_object_t _head, dispatch_object_t _tail, int n) { struct dispatch_object_s *hd = _head._do, *tl = _tail._do; if (unlikely(os_mpsc_push_list(os_mpsc(dq, dq_items), hd, tl, do_next))) { return _dispatch_root_queue_poke(dq, n, 0); } }

  • _dispatch_root_queue_poke

_dispatch_root_queue_poke

  • _dispatch_root_queue_poke_slow

_dispatch_root_queue_poke_slow _dispatch_root_queues_init方法使用了單例。

static inline void _dispatch_root_queues_init(void) { dispatch_once_f(&_dispatch_root_queues_pred, NULL, _dispatch_root_queues_init_once); }

在該方法中,採用單例的方式進行了執行緒池的初始化處理、工作佇列的配置、工作佇列的初始化等工作。同時這裡有一個關鍵的設定,執行函式的設定,也就是將任務執行的函式被統一設定成了_dispatch_worker_thread2。見下圖: _dispatch_root_queues_init_once

  • 呼叫堆疊驗證

堆疊資訊

呼叫執行是通過workloop工作迴圈呼叫起來的,也就是說並不是及時呼叫的,而是通過os完成呼叫,說明非同步呼叫的關鍵是在需要執行的時候能夠獲取對應的方法,進行非同步處理,而同步函式是直接呼叫。

在上面的流程中_dispatch_root_queue_poke_slow方法,還沒有繼續分析,現在就去分析,如果是全域性佇列,此時會建立執行緒進行執行任務 全域性佇列處理 對執行緒池進行處理,從執行緒池中獲取執行緒,執行任務,同時判斷執行緒池的變化 執行緒池進行處理 remaining可以理解為當前可用執行緒數,當可用執行緒數等於0時,執行緒池已滿pthread pool is full,直接return。底層通過pthread完成執行緒的開闢 在這裡插入圖片描述 就是_dispatch_worker_thread2是通過pthread完成oc_atmoic原子觸發

那麼我們的執行緒可以開闢多少執行緒條呢?

執行緒池初始化

佇列執行緒池的大小為:dgq_thread_pool_sizedgq_thread_pool_size = thread_pool_size ,預設大小如下: DISPATCH_WORKQ_MAX_PTHREAD_COUNT 255表示理論上執行緒池的最大數量。但是實際能開闢多少呢,這個不確定。在蘋果官方完整Thread Management中,有相關的說明,輔助執行緒的最小允許堆疊大小為 16KB,並且堆疊大小必須是4KB 的倍數。見下圖: Thread Management 也就是說,一個輔助執行緒的棧空間是512KB,而一個執行緒所佔用的最小空間是16KB,也就是說棧空間一定的情況下,開闢執行緒所需的記憶體越大,所能開闢的執行緒數就越小。針對一個4GB記憶體的iOS真機來說,記憶體分為核心態和使用者態,如果核心態全部用於建立執行緒,也就是1GB的空間,也就是說最多能開闢1024KB / 16KB個執行緒。當然這也只是一個理論值。

三、 單例

上面提到了單例,那麼接下來就去分析一下單例 來看看簡單的單例使用:

```objc static dispatch_once_t token;

dispatch_once(&token, ^{ // 程式碼執行 }); ```

  • 單例的定義如下:

單例定義

```objc void _dispatch_once(dispatch_once_t predicate, DISPATCH_NOESCAPE dispatch_block_t block) { if (DISPATCH_EXPECT(predicate, ~0l) != ~0l) { dispatch_once(predicate, block); } else { dispatch_compiler_barrier(); } DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l); }

undef dispatch_once

define dispatch_once _dispatch_once

endif

endif // DISPATCH_ONCE_INLINE_FASTPATH

```

針對不同的情況作了一些特殊處理,比如柵欄函式等,這裡只分析dispatch_once,進入dispatch_once實現 dispatch_once 單例是隻會執行一次,那麼這裡就是利用 val引數來進行控制的,接著去dispatch_once_f裡面看看 在這裡插入圖片描述l的底層原子性進行關聯,關聯到uintptr_t v的一個變數,通過os_atomic_load從底層取出,關聯到變數v上。如果v這個值等於DLOCK_ONCE_DONE,也就是已經處理過一次了,就會直接return返回

  • _dispatch_once_gate_tryenter

c++ static inline bool _dispatch_once_gate_tryenter(dispatch_once_gate_t l) { return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED, (uintptr_t)_dispatch_lock_value_for_self(), relaxed); }

_dispatch_once_gate_tryenter裡面是進行原子操作,就是鎖的處理,如果之前沒有執行過,原子處理會比較它狀態,進行解鎖,最終會返回一個bool值,多執行緒情況下,只有一個能夠獲取鎖返回yes

c++ if (_dispatch_once_gate_tryenter(l)) { return _dispatch_once_callout(l, ctxt, func); }

通過_dispatch_lock_value_for_self上了一把鎖,保證多執行緒安全。如果返回yes,就會執行_dispatch_once_callout方法,執行單例對應的任務,並對外廣播

  • _dispatch_once_callout

c++ static void _dispatch_once_callout(dispatch_once_gate_t l, void *ctxt, dispatch_function_t func) { _dispatch_client_callout(ctxt, func); _dispatch_once_gate_broadcast(l); }

  • _dispatch_client_callout執行任務
  • _dispatch_once_gate_broadcast對外廣播,標記為 done
  • _dispatch_once_gate_broadcast廣播

_dispatch_once_gate_broadcasttoken通過原子比對,如果不是done,則設為done。同時對_dispatch_once_gate_tryenter方法中的鎖進行處理。

  • _dispatch_once_mark_done

_dispatch_once_mark_done os_atomic_cmpxchg是一個巨集定義,先進行比較再改變,先比較 dgo,在設定標記為DLOCK_ONCE_DONE也就是doneos_atomic_cmpxchg

token標記為done之後,就會直接返回,如存在多執行緒處理,沒有獲取鎖的情況,就會呼叫_dispatch_once_wait,如下下: 單例執行方法 _dispatch_once_wait,進行等待,這裡開啟了自旋鎖,內部進行原子處理,在loop過程中,如果發現已經被其他執行緒設定once_done了,則會進行放棄處理 _dispatch_once_wait 那麼任務的執行交給誰了呢? 堆疊資訊 通過列印堆疊資訊,發現是交給了下層的執行緒,通過一些包裝,給了底層的pthread在這裡插入圖片描述 這就可以說 GCD底層是封裝了pthread,不管是iOS還是 Java都是封裝了底層的通用執行緒機制pthread

這裡的執行是通過工作迴圈workloop,工作迴圈的調起受 OS(受 CPU排程執行的。)管控的,非同步執行緒的非同步體現在哪裡呢?就是體現在是否可以獲得,而不是立即執行,而同步函式是直接呼叫執行的,而這裡並沒有看到非同步的直接呼叫執行。

四、 sync 和 async 的區別

  • 是否可以開啟新的執行緒執行任務
  • 任務的回撥是否具有非同步行、同步性
  • 是否產生死鎖問題

五、 死鎖 原始碼分析

在前面篇幅的分析中,我們得知,同步 sync函式的流程是: - _dispatch_sync_f -- > - _dispatch_sync_f_inline -- > - _dispatch_barrier_sync_f

_dispatch_sync_f_inline 走到_dispatch_barrier_sync_f流程中,這與上篇部落格的分析是一致的,因為這裡dq_width=1,所以是序列佇列,如果是併發佇列,則會走到_dispatch_sync_f_slow,現在去_dispatch_barrier_sync_f方法裡面看看

  • _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_barrier_sync_f_inline 在這個方法裡面,會對佇列進行判斷,是否存在等待或者掛起狀態

c++ //判斷是否掛起、等待 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); }

在之前的部落格裡面也提到了死鎖相關的內容,出現死鎖會報和_dispatch_sync_f_slow相關的錯誤,如下:

死鎖 雖然死鎖會走_dispatch_sync_f_slow方法,但是死鎖的報錯不是_dispatch_sync_f_slow這個報錯,而是如下圖中所示的0處報錯了

死鎖報錯

真報錯的是__DISPATCH_WAIT_FOR_QUEUE__,那麼現在去驗證一下

  • _dispatch_sync_f_slow

_dispatch_sync_f_slow_dispatch_sync_f_slow方法內部,我們發現了剛剛死鎖報錯的__DISPATCH_WAIT_FOR_QUEUE__,現在去內部看看

  • __DISPATCH_WAIT_FOR_QUEUE__

DISPATCH_WAIT_FOR_QUEUE

__DISPATCH_WAIT_FOR_QUEUE__內部,發現了和死鎖報錯資訊基本一樣,意思是:

dispatch_sync在當前執行緒已經擁有的佇列上呼叫 ,對不起兄弟,我已經擁有她了,你來晚一步了

c++ 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"); }

這個dsc_waiter是由前面_dispatch_sync_f_slow方法裡面傳過來來的

dsc_waiter

_dispatch_tid_self()是執行緒id,定義如下

_dispatch_tid_self()

_dispatch_thread_port是執行緒的通道,現在再去看看執行緒狀態的匹配

c++ //狀態 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"); }

  • _dq_state_drain_locked_by

c++ 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); }

  • _dispatch_lock_is_locked_by

c++ static inline bool _dispatch_lock_is_locked_by(dispatch_lock lock_value, dispatch_tid tid) { // equivalent to _dispatch_lock_owner(lock_value) == tid return ((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0; }

  • DLOCK_OWNER_MASK

```c++

define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc)

```

這裡就是死鎖的判斷:異或再作操作,也就是結果為0就是死鎖。翻譯一下就是dq_state ^ dsc->dsc_waiter的結果為 0再和DLOCK_OWNER_MASK操作等於0

那麼dq_state ^ dsc->dsc_waiter的結果什麼情況下會為 0呢?異或是相同為0,因為DLOCK_OWNER_MASK是一個非常大的整數,所以dq_statedsc->dsc_waiter都是為0

當前佇列裡面要等待的執行緒 id和我呼叫的是一樣,我已經處於等待狀態,你現在有新的任務過來需要使用我去執行,這樣產生了矛盾,進入相互等待狀態,進而產生死鎖。這就是序列佇列執行同步任務產生死鎖的原因!

「其他文章」