回顧
在上篇博客已經(jīng)對GCD
的sync
同步函數(shù)產(chǎn)生死鎖的情況,進行了底層的源碼探索分析冠胯,那么本篇博客繼續(xù)源碼的探索分析火诸!
iOS底層探索之多線程(六)—GCD源碼分析(sync 同步函數(shù)、async 異步函數(shù))
1. 全局并發(fā)隊列+同步函數(shù)
dq->dq_width == 1
為串行隊列荠察,那么并發(fā)隊列該怎么走呢置蜀?
如下圖,走的是下面的框框中流程
但是這么多的分支悉盆,到底是走的哪一個呢盯荤?通過對_dispatch_sync_f_slow
、 _dispatch_sync_recurse
焕盟、_dispatch_introspection_sync_begin
秋秤、_dispatch_sync_invoke_and_complete
方法下符號斷點,進行跟蹤調(diào)試脚翘。
- 符號斷點調(diào)試
通過下符號斷點跟蹤灼卢,發(fā)現(xiàn)走了_dispatch_sync_f_slow
,如下圖所示:
通過閱讀源碼堰怨,發(fā)現(xiàn)一個有意思的事情芥玉,就是_dispatch_sync_invoke_and_complete
方法
- _dispatch_sync_invoke_and_complete
在這個_dispatch_sync_invoke_and_complete
方法的第三個參數(shù)是func
也是需要執(zhí)行的任務(wù),但是 func
的后面的整體也是一個參數(shù)备图,也就是DISPATCH_TRACE_ARG( _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags))
整體為一個參數(shù)灿巧,這就有意思了,中間居然沒有逗號分隔開揽涮。老鐵抠藕,你這很特別啊蒋困!夠長的岸芩啤!
那么去DISPATCH_TRACE_ARG
定義看看
在DISPATCH_TRACE_ARG
的宏定義里面,你們有沒有發(fā)現(xiàn)零院,這里居然把逗號
放在了里面溉跃,好家伙,宏定義里面還可以這么玩告抄,蘋果工程師還真有意思哈撰茎!
通過全局的搜索,發(fā)現(xiàn)這個宏定義有兩處打洼,一個有逗號龄糊,一個沒有逗號,這就是根據(jù)不同的條件募疮,進行設(shè)置炫惩,相當(dāng)于是一個可選的參數(shù)
,這一波操作又是非常的細(xì)節(jié)了阿浓!
既然下符合斷點會走_dispatch_sync_f_slow
方法他嚷,現(xiàn)在就去看看這個方法
- _dispatch_sync_f_slow
這里又是很多的分支,又通過下符合斷點搔扁,發(fā)現(xiàn)走的是_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
之后調(diào)用callout
執(zhí)行爸舒,最后再pop
蟋字,所以可以同步的執(zhí)行任務(wù)
2. 異步函數(shù)
dispatch_async
異步函數(shù)的任務(wù)稿蹲,是包裝在 qos
里面的,那么現(xiàn)在跟蹤流程鹊奖,去看看
-
dispatch_async
dispatch_async - _dispatch_continuation_async
- dx_push
搜索dx_push
調(diào)用的地方
這里就先去看看并發(fā)隊列里面的dq_push
吧苛聘,
- _dispatch_lane_concurrent_push
這里if
里面有對柵欄函數(shù)
(_dispatch_object_is_barrier)的判斷,柵欄函數(shù)這里就不分析了忠聚,后續(xù)的博客里面會分析的设哗。
在_dispatch_lane_concurrent_push
里面會去調(diào)用_dispatch_lane_push
方法,在上面搜索dx_push
的圖里面两蟀,可以看到网梢,在串行隊列里面是直接調(diào)用了_dispatch_lane_push
,也就是說串行
和并發(fā)
都會走這個方法赂毯。
- _dispatch_lane_push
最后去調(diào)用dx_wakeup
战虏,再去搜索看看
dx_wakeup
是一個宏定義,看看dq_wakeup
哪里調(diào)用了
如上圖可以發(fā)現(xiàn)党涕,串行和并發(fā)都是_dispatch_lane_wakeup
烦感,全局的是_dispatch_root_queue_wakeup
- _dispatch_queue_wakeup
通過下符合斷點會走_dispatch_lane_class_barrier_complete
_dispatch_lane_class_barrier_complete
里面循環(huán)遞歸一些,操作膛堤,還看到了一個系統(tǒng)的函數(shù)os_atomic_rmw_loop2o
手趣,在這個方法里面要么返回dx_wakeup
或者做其他的一些處理。
全局并發(fā)隊列會調(diào)用_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_queues_init方法
使用了單例。
static inline void
_dispatch_root_queues_init(void)
{
dispatch_once_f(&_dispatch_root_queues_pred, NULL,
_dispatch_root_queues_init_once);
}
在該方法中中符,采用單例的方式進行了線程池的初始化處理潜圃、工作隊列的配置、工作隊列的初始化等工作舟茶。同時這里有一個關(guān)鍵的設(shè)置谭期,執(zhí)行函數(shù)的設(shè)置,也就是將任務(wù)執(zhí)行的函數(shù)被統(tǒng)一設(shè)置成了_dispatch_worker_thread2
吧凉。見下圖:
調(diào)用執(zhí)行是通過workloop
工作循環(huán)調(diào)用起來的隧出,也就是說并不是及時調(diào)用的,而是通過os
完成調(diào)用阀捅,說明異步調(diào)用的關(guān)鍵是在需要執(zhí)行的時候能夠獲取對應(yīng)的方法胀瞪,進行異步處理,而同步函數(shù)是直接調(diào)用饲鄙。
在上面的流程中_dispatch_root_queue_poke_slow
方法凄诞,還沒有繼續(xù)分析,現(xiàn)在就去分析忍级,如果是全局隊列帆谍,此時會創(chuàng)建線程進行執(zhí)行任務(wù)
對線程池進行處理,從線程池中獲取線程轴咱,執(zhí)行任務(wù)汛蝙,同時判斷線程池的變化
remaining
可以理解為當(dāng)前可用線程數(shù),當(dāng)可用線程數(shù)等于0
時朴肺,線程池已滿pthread pool is full
窖剑,直接return
。底層通過pthread
完成線程的開辟
就是_dispatch_worker_thread2
是通過pthread
完成oc_atmoic
原子觸發(fā)
那么我們的線程可以開辟多少線程條呢戈稿?
隊列線程池的大小為:dgq_thread_pool_size
西土。dgq_thread_pool_size = thread_pool_size
,默認(rèn)大小如下:
255
表示理論上線程池的最大數(shù)量鞍盗。但是實際能開辟多少呢需了,這個不確定。在蘋果官方完整Thread Management中橡疼,有相關(guān)的說明援所,輔助線程的最小允許堆棧大小為 16
KB,并且堆棧大小必須是4
KB 的倍數(shù)欣除。見下圖:
也就是說住拭,一個輔助線程的棧空間是
512KB
,而一個線程所占用的最小空間是16KB
滔岳,也就是說椄苡椋空間一定的情況下,開辟線程所需的內(nèi)存越大谱煤,所能開辟的線程數(shù)就越小摊求。針對一個4GB
內(nèi)存的iOS
真機來說,內(nèi)存分為內(nèi)核態(tài)和用戶態(tài)刘离,如果內(nèi)核態(tài)全部用于創(chuàng)建線程室叉,也就是1GB
的空間,也就是說最多能開辟1024KB / 16KB
個線程硫惕。當(dāng)然這也只是一個理論值茧痕。
3. 單例
上面提到了單例,那么接下來就去分析一下單例
來看看簡單的單例使用:
static dispatch_once_t token;
dispatch_once(&token, ^{
// 代碼執(zhí)行
});
-
單例的定義如下:
單例定義
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
針對不同的情況作了一些特殊處理恼除,比如柵欄函數(shù)
等踪旷,這里只分析dispatch_once
,進入dispatch_once
實現(xiàn)
單例是只會執(zhí)行一次豁辉,那么這里就是利用
val
參數(shù)來進行控制的令野,接著去dispatch_once_f
里面看看
對
l
的底層原子性進行關(guān)聯(lián),關(guān)聯(lián)到uintptr_t v
的一個變量徽级,通過os_atomic_load
從底層取出气破,關(guān)聯(lián)到變量v
上。如果v
這個值等于DLOCK_ONCE_DONE
灰追,也就是已經(jīng)處理過一次了堵幽,就會直接return
返回
- _dispatch_once_gate_tryenter
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
里面是進行原子操作,就是鎖的處理弹澎,如果之前沒有執(zhí)行過,原子處理會比較它狀態(tài)努咐,進行解鎖苦蒿,最終會返回一個bool
值,多線程情況下渗稍,只有一個能夠獲取鎖返回yes
佩迟。
if (_dispatch_once_gate_tryenter(l)) {
return _dispatch_once_callout(l, ctxt, func);
}
通過_dispatch_lock_value_for_self
上了一把鎖,保證多線程安全竿屹。如果返回yes
报强,就會執(zhí)行_dispatch_once_callout
方法,執(zhí)行單例對應(yīng)的任務(wù)拱燃,并對外廣播
- _dispatch_once_callout
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
執(zhí)行任務(wù)_dispatch_once_gate_broadcast
對外廣播秉溉,標(biāo)記為done
_dispatch_once_gate_broadcast
廣播
將
token
通過原子比對,如果不是done
,則設(shè)為done
召嘶。同時對_dispatch_once_gate_tryenter
方法中的鎖進行處理父晶。
- _dispatch_once_mark_done
os_atomic_cmpxchg
是一個宏定義,先進行比較再改變弄跌,先比較 dgo
甲喝,在設(shè)置標(biāo)記為DLOCK_ONCE_DONE
也就是done
當(dāng)token
標(biāo)記為done
之后,就會直接返回铛只,如存在多線程處理埠胖,沒有獲取鎖的情況,就會調(diào)用_dispatch_once_wait
淳玩,如下下:
_dispatch_once_wait
押袍,進行等待,這里開啟了自旋鎖
凯肋,內(nèi)部進行原子處理谊惭,在loop
過程中,如果發(fā)現(xiàn)已經(jīng)被其他線程設(shè)置once_done
了侮东,則會進行放棄處理
那么任務(wù)的執(zhí)行交給誰了呢圈盔?
通過打印堆棧信息,發(fā)現(xiàn)是交給了下層的線程悄雅,通過一些包裝驱敲,給了底層的pthread
這就可以說
GCD
底層是封裝了pthread
,不管是iOS
還是 Java
都是封裝了底層的通用線程機制pthread
宽闲。
這里的執(zhí)行是通過工作循環(huán)workloop
,工作循環(huán)的調(diào)起受 OS(受 CPU調(diào)度執(zhí)行的众眨。)管控的,異步線程的異步體現(xiàn)在哪里呢容诬?就是體現(xiàn)在是否可以獲得娩梨,而不是立即執(zhí)行,而同步函數(shù)是直接調(diào)用執(zhí)行的览徒,而這里并沒有看到異步的直接調(diào)用執(zhí)行狈定。
更多內(nèi)容持續(xù)更新
?? 喜歡就點個贊吧????
?? 覺得有收獲的,可以來一波习蓬,收藏+關(guān)注纽什,評論 + 轉(zhuǎn)發(fā),以免你下次找不到我????
??歡迎大家留言交流躲叼,批評指正芦缰,互相學(xué)習(xí)??,提升自我??