iOS底層探索之多線程(八)—GCD源碼分析(函數(shù)的同步性物舒、異步性色洞、單例)

回顧

在上篇博客已經(jīng)對GCDsync同步函數(shù)產(chǎn)生死鎖的情況,進行了底層的源碼探索分析冠胯,那么本篇博客繼續(xù)源碼的探索分析火诸!

在這里插入圖片描述

iOS底層探索之多線程(一)—進程和線程

iOS底層探索之多線程(二)—線程和鎖

iOS底層探索之多線程(三)—初識GCD

iOS底層探索之多線程(四)—GCD的隊列

iOS底層探索之多線程(五)—GCD不同隊列源碼分析

iOS底層探索之多線程(六)—GCD源碼分析(sync 同步函數(shù)、async 異步函數(shù))

iOS底層探索之多線程(七)—GCD源碼分析(死鎖的原因)

1. 全局并發(fā)隊列+同步函數(shù)

dq->dq_width == 1為串行隊列荠察,那么并發(fā)隊列該怎么走呢置蜀?
如下圖,走的是下面的框框中流程

_dispatch_sync_f_inline

但是這么多的分支悉盆,到底是走的哪一個呢盯荤?通過對_dispatch_sync_f_slow_dispatch_sync_recurse 焕盟、_dispatch_introspection_sync_begin 秋秤、_dispatch_sync_invoke_and_complete方法下符號斷點,進行跟蹤調(diào)試脚翘。

  • 符號斷點調(diào)試
符號斷點調(diào)試

通過下符號斷點跟蹤灼卢,發(fā)現(xiàn)走了_dispatch_sync_f_slow,如下圖所示:

斷點在_dispatch_sync_f_slow處

通過閱讀源碼堰怨,發(fā)現(xiàn)一個有意思的事情芥玉,就是_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方法的第三個參數(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

DISPATCH_TRACE_ARG的宏定義里面,你們有沒有發(fā)現(xiàn)零院,這里居然把逗號放在了里面溉跃,好家伙,宏定義里面還可以這么玩告抄,蘋果工程師還真有意思哈撰茎!

DISPATCH_TRACE_ARG

通過全局的搜索,發(fā)現(xiàn)這個宏定義有兩處打洼,一個有逗號龄糊,一個沒有逗號,這就是根據(jù)不同的條件募疮,進行設(shè)置炫惩,相當(dāng)于是一個可選的參數(shù),這一波操作又是非常的細(xì)節(jié)了阿浓!

既然下符合斷點會走_dispatch_sync_f_slow方法他嚷,現(xiàn)在就去看看這個方法

  • _dispatch_sync_f_slow
_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
_dispatch_continuation_async
  • dx_push
dx_push

搜索dx_push調(diào)用的地方

在這里插入圖片描述

這里就先去看看并發(fā)隊列里面的dq_push吧苛聘,

  • _dispatch_lane_concurrent_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
_dispatch_lane_push

最后去調(diào)用dx_wakeup战虏,再去搜索看看

dx_wakeup

dx_wakeup 是一個宏定義,看看dq_wakeup哪里調(diào)用了

dx_wakeup調(diào)用地方

如上圖可以發(fā)現(xiàn)党涕,串行和并發(fā)都是_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里面循環(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_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吧凉。見下圖:

_dispatch_root_queues_init_once

調(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)大小如下:

DISPATCH_WORKQ_MAX_PTHREAD_COUNT

255表示理論上線程池的最大數(shù)量鞍盗。但是實際能開辟多少呢需了,這個不確定。在蘋果官方完整Thread Management中橡疼,有相關(guān)的說明援所,輔助線程的最小允許堆棧大小為 16KB,并且堆棧大小必須是4KB 的倍數(shù)欣除。見下圖:

Thread Management

也就是說住拭,一個輔助線程的棧空間是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)

dispatch_once

單例是只會執(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廣播

_dispatch_once_gate_broadcast

token通過原子比對,如果不是done,則設(shè)為done召嘶。同時對_dispatch_once_gate_tryenter方法中的鎖進行處理父晶。

  • _dispatch_once_mark_done
_dispatch_once_mark_done

os_atomic_cmpxchg是一個宏定義,先進行比較再改變弄跌,先比較 dgo甲喝,在設(shè)置標(biāo)記為DLOCK_ONCE_DONE也就是done

os_atomic_cmpxchg

當(dāng)token標(biāo)記為done之后,就會直接返回铛只,如存在多線程處理埠胖,沒有獲取鎖的情況,就會調(diào)用_dispatch_once_wait淳玩,如下下:

單例執(zhí)行方法

_dispatch_once_wait押袍,進行等待,這里開啟了自旋鎖凯肋,內(nèi)部進行原子處理谊惭,在loop過程中,如果發(fā)現(xiàn)已經(jīng)被其他線程設(shè)置once_done了侮东,則會進行放棄處理

_dispatch_once_wait

那么任務(wù)的執(zhí)行交給誰了呢圈盔?

堆棧信息

通過打印堆棧信息,發(fā)現(xiàn)是交給了下層的線程悄雅,通過一些包裝驱敲,給了底層的pthread

image

這就可以說 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í)??,提升自我??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末枫慷,一起剝皮案震驚了整個濱河市让蕾,隨后出現(xiàn)的幾起案子浪规,更是在濱河造成了極大的恐慌,老刑警劉巖涕俗,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罗丰,死亡現(xiàn)場離奇詭異,居然都是意外死亡再姑,警方通過查閱死者的電腦和手機萌抵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來元镀,“玉大人绍填,你說我怎么就攤上這事∑芤桑” “怎么了讨永?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長遇革。 經(jīng)常有香客問我卿闹,道長,這世上最難降的妖魔是什么萝快? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任锻霎,我火速辦了婚禮,結(jié)果婚禮上揪漩,老公的妹妹穿的比我還像新娘旋恼。我一直安慰自己,他們只是感情好奄容,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布冰更。 她就那樣靜靜地躺著,像睡著了一般昂勒。 火紅的嫁衣襯著肌膚如雪蜀细。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天叁怪,我揣著相機與錄音审葬,去河邊找鬼。 笑死奕谭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的痴荐。 我是一名探鬼主播血柳,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼生兆!你這毒婦竟也來了难捌?” 一聲冷哼從身側(cè)響起膝宁,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎根吁,沒想到半個月后员淫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡击敌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年介返,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沃斤。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡圣蝎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出衡瓶,到底是詐尸還是另有隱情徘公,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布哮针,位于F島的核電站关面,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏十厢。R本人自食惡果不足惜等太,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寿烟。 院中可真熱鬧澈驼,春花似錦、人聲如沸筛武。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽徘六。三九已至内边,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間待锈,已是汗流浹背漠其。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留竿音,地道東北人和屎。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像春瞬,于是被迫代替她去往敵國和親柴信。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容