iOS之GCD底層探索

一抱完、引言

前邊一篇文章我們已經(jīng)大致介紹了GCD的有些概念和函數(shù)的執(zhí)行。接下來(lái)讓我們繼續(xù)帶著探索的心里去學(xué)習(xí)刃泡,繼續(xù)前行巧娱,繼續(xù)介紹線程是如何開辟和創(chuàng)建的,又是什么時(shí)候去執(zhí)行相關(guān)的調(diào)度任務(wù)烘贴。接下來(lái)讓我們從相關(guān)的概念入手

二 禁添、線程和任務(wù)

2.1 GCD的優(yōu)勢(shì)

  • 1 GCD 是蘋果公司為多核的并行運(yùn)算提出的解決方案
  • 2 GCD 會(huì)自動(dòng)利用更多的CPU內(nèi)核(比如雙核、四核)
  • 3 GCD 會(huì)自動(dòng)管理線程的生命周期(創(chuàng)建線程桨踪、調(diào)度任務(wù)老翘、銷毀線程)
  • 4 程序員只需要告訴 GCD 想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼

2.2 任務(wù)

  • 1 任務(wù)使用 block 封裝
  • 2 任務(wù)的 block 沒有參數(shù)也沒有返回值

2.3 同步任務(wù)

  • 1 函數(shù): dispatch_sync
  • 2 必須等待當(dāng)前語(yǔ)句執(zhí)行完畢,才會(huì)執(zhí)行下一條語(yǔ)句
  • 3 不會(huì)開啟線程
  • 4 在當(dāng)前執(zhí)行 block 的任務(wù)

2.4異步任務(wù)

  • 1 異步 dispatch_async
  • 2 不用等待當(dāng)前語(yǔ)句執(zhí)行完畢铺峭,就可以執(zhí)行下一條語(yǔ)句
  • 3 會(huì)開啟線程執(zhí)行 block 的任務(wù)
  • 4 異步是多線程的代名詞

2.5主隊(duì)列

  • 1 函數(shù):dispatch_get_main_queue();
  • 2 特殊的串行隊(duì)列
  • 3專門用來(lái)在主線程上調(diào)度任務(wù)的隊(duì)列
  • 4不會(huì)開啟線程
  • 5 如果當(dāng)前主線程正在執(zhí)行任務(wù)墓怀,那么無(wú)論主隊(duì)列中當(dāng)前被添加了什么任務(wù),都不會(huì)被調(diào)度

2.6.全局隊(duì)列

  • 1 函數(shù):dispatch_get_global_queue(0,0);
  • 2 為了方便程序員使用卫键,蘋果提供了全局隊(duì)列
  • 3全局隊(duì)列是一個(gè)并發(fā)隊(duì)列
  • 4 在使用多線程開發(fā)時(shí)傀履,如果對(duì)隊(duì)列沒有特殊需求,在執(zhí)行異步任務(wù)時(shí)莉炉,可以直接使用全局隊(duì)列

2.7 隊(duì)列和任務(wù)搭配

  • 1 同步串行隊(duì)列:不開啟新線程,串行執(zhí)行任務(wù);
  • 2 同步并發(fā)隊(duì)列:不開啟新線程,串行執(zhí)行任務(wù);
  • 3同步主隊(duì)列:如果在主線程調(diào)用,造成死鎖;如果在其他線程調(diào)用,不開啟新線程,串行執(zhí)行,在主線程串行執(zhí)行任務(wù);
  • 4 異步串行隊(duì)列:開啟新線程,串行執(zhí)行任務(wù);
  • 5 異步并發(fā)隊(duì)列:開啟新線程,并發(fā)執(zhí)行任務(wù);
  • 6 異步主隊(duì)列:不開啟新線程,在主線程串行執(zhí)行任務(wù)
  • 7 死鎖:

三钓账、GCD 底層源碼分析

3.1 dispatch_async(異步函數(shù))在何時(shí)開辟線程

我們都知道主線程和同步線程不會(huì)開辟子線程,但是當(dāng)我們用dispatch_async(異步函數(shù))的時(shí)候絮宁,線程是如何開辟的呢梆暮?是何時(shí)執(zhí)行的調(diào)度任務(wù)呢?這些都是我們?cè)谑褂肎CD的情況下很少去考慮的問題绍昂,為了弄清楚相關(guān)的原理啦粹,讓我們?cè)敿?xì)去研究一下相關(guān)的流程。

我們?cè)跍y(cè)試Demo中打印一個(gè)簡(jiǎn)單的語(yǔ)句治专,并且進(jìn)行相應(yīng)的斷點(diǎn)調(diào)試卖陵,然后打印出相應(yīng)的堆棧信息

    dispatch_async(_queue, ^{
        
        NSLog(@"1111111");
        
    })

在控制臺(tái)進(jìn)行bt打印所有的堆棧信息,可以得出相應(yīng)的執(zhí)行步驟如下

異步函數(shù)執(zhí)行步驟.png

從打印結(jié)果看我們知道異步線程會(huì)執(zhí)行_dispatch_lane_invoke,有興趣的同學(xué)可以加一個(gè)符號(hào)斷點(diǎn)進(jìn)行調(diào)試张峰,程序一定會(huì)執(zhí)行到此為止的泪蔫,我們進(jìn)入相應(yīng)的定義

void
_dispatch_lane_invoke(dispatch_lane_t dq, dispatch_invoke_context_t dic,
        dispatch_invoke_flags_t flags)
{
    _dispatch_queue_class_invoke(dq, dic, flags, 0, _dispatch_lane_invoke2);
}

然后到_dispatch_lane_serial_drain的定義

dispatch_queue_wakeup_target_t
_dispatch_lane_serial_drain(dispatch_lane_class_t dqu,
        dispatch_invoke_context_t dic, dispatch_invoke_flags_t flags,
        uint64_t *owned)
{
    flags &= ~(dispatch_invoke_flags_t)DISPATCH_INVOKE_REDIRECTING_DRAIN;
    return _dispatch_lane_drain(dqu._dl, dic, flags, owned, true);
}

最終會(huì)走到_dispatch_root_queue_poke_slow方法,進(jìn)行相應(yīng)的線程創(chuàng)建喘批,
其中創(chuàng)建線程的代碼如下

#if !defined(_WIN32)
    pthread_attr_t *attr = &pqc->dpq_thread_attr;
    pthread_t tid, *pthr = &tid;
#if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
    if (unlikely(dq == &_dispatch_mgr_root_queue)) {
        pthr = _dispatch_mgr_root_queue_init();
    }
#endif
    do {
        _dispatch_retain(dq); // released in _dispatch_worker_thread
        while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
            if (r != EAGAIN) {
                (void)dispatch_assume_zero(r);
            }
            _dispatch_temporary_resource_shortage();
        }
    } while (--remaining);
#else // defined(_WIN32)
#if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
    if (unlikely(dq == &_dispatch_mgr_root_queue)) {
        _dispatch_mgr_root_queue_init();
    }

從最終的線程創(chuàng)建我們知道撩荣,線程創(chuàng)建過程當(dāng)中,不斷的判斷需要?jiǎng)?chuàng)建多少線程饶深,每創(chuàng)建一個(gè)就減少一個(gè)餐曹,從而調(diào)用pthread_create的API進(jìn)行創(chuàng)建。這就是GCD線程創(chuàng)建的過程敌厘;

3.2 dispatch_barrier_async(柵欄函數(shù))

dispatch_barrier_async(柵欄函數(shù))他台猴,通過其命名我們就知道是攔截的意思。也就是在柵欄函數(shù)之前的任務(wù)執(zhí)行完成后俱两,才能執(zhí)行后邊的任務(wù)饱狂。先看一下官方文檔

dispatch_barrier_sync: Submits a barrier block object for execution and waits until that block completes.(提交一個(gè)柵欄函數(shù)在執(zhí)行中,它會(huì)等待柵欄函數(shù)執(zhí)行完)

dispatch_barrier_async: Submits a barrier block for asynchronous execution and returns immediately.(提交一個(gè)柵欄函數(shù)在異步執(zhí)行中,它會(huì)立馬返回)

作者理解:dispatch_barrier_sync 需要等待柵欄執(zhí)行完才會(huì)執(zhí)行柵欄后面的任務(wù),而dispatch_barrier_async 無(wú)需等待柵欄執(zhí)行完,會(huì)繼續(xù)往下走(保留在隊(duì)列里)

原因:在同步柵欄時(shí)柵欄函數(shù)在主線程中執(zhí)行,而異步柵欄中開辟了子線程?hào)艡诤瘮?shù)在子線程中執(zhí)行

我們看看dispatch_barrier_async異步柵欄函數(shù)的執(zhí)行結(jié)果

dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"123");
    });
    /* 2. 柵欄函數(shù) */ // - dispatch_barrier_sync
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
    });
    /* 3. 異步函數(shù) */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"加載那么多,喘口氣!!!");
    });
    NSLog(@"**********起來(lái)干!!");

執(zhí)行結(jié)果是


異步柵欄函數(shù)的執(zhí)行結(jié)果.png

我們?cè)俅慰纯?code>dispatch_barrier_sync同步柵欄函數(shù)的執(zhí)行結(jié)果:

  dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"123");
    });
    /* 2. 柵欄函數(shù) */ // - dispatch_barrier_sync
    dispatch_barrier_sync(concurrentQueue, ^{
        NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
    });
    /* 3. 異步函數(shù) */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"加載那么多,喘口氣!!!");
    });
    NSLog(@"**********起來(lái)干!!");
 

打印結(jié)果如下
同步柵欄函數(shù)的執(zhí)行.png
結(jié)論 dispatch_barrier_sync 需要等待柵欄執(zhí)行完才會(huì)執(zhí)行柵欄后面的任務(wù),而dispatch_barrier_async 無(wú)需等待柵欄執(zhí)行完,會(huì)繼續(xù)往下走(保留在隊(duì)列里)

3.3 dispatch_semaphore_t(信號(hào)量)

信號(hào)量是通過信號(hào)的數(shù)量來(lái)控制相應(yīng)的任務(wù)調(diào)度,需要設(shè)置一個(gè)固定的信號(hào)量宪彩,有可以操作的任務(wù)調(diào)度才可以執(zhí)行休讳,就好比停車場(chǎng)的空車位數(shù)一樣,數(shù)量一定尿孔,如果有空就可以進(jìn)行停車俊柔,如果車位已滿就讓后來(lái)的人等待筹麸。其中信號(hào)量控制有三個(gè)非常重要的函數(shù)

dispatch_semaphore_create
dispatch_semaphore_wait
dispatch_semaphore_signal
  • 1 dispatch_semaphore_create(long value);和GCD的group等用法一致,這個(gè)函數(shù)是創(chuàng)建一個(gè)dispatch_semaphore_類型的信號(hào)量雏婶,并且創(chuàng)建的時(shí)候需要指定信號(hào)量的大小物赶。

  • 2 dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待信號(hào)量。該函數(shù)會(huì)對(duì)信號(hào)量進(jìn)行減1操作尚骄。如果減1后信號(hào)量小于0(即減1前信號(hào)量值為0)块差,那么該函數(shù)就會(huì)一直等待,也就是不返回(相當(dāng)于阻塞當(dāng)前線程)倔丈,直到該函數(shù)等待的信號(hào)量的值大于等于1憨闰,該函數(shù)會(huì)對(duì)信號(hào)量的值進(jìn)行減1操作,然后返回需五。

  • 3 dispatch_semaphore_signal(dispatch_semaphore_t deem); 發(fā)送信號(hào)量鹉动。該函數(shù)會(huì)對(duì)信號(hào)量的值進(jìn)行加1操作。

通常等待信號(hào)量和發(fā)送信號(hào)量的函數(shù)是成對(duì)出現(xiàn)的宏邮。并發(fā)執(zhí)行任務(wù)時(shí)候泽示,在當(dāng)前任務(wù)執(zhí)行之前,用dispatch_semaphore_wait函數(shù)進(jìn)行等待(阻塞)蜜氨,直到上一個(gè)任務(wù)執(zhí)行完畢后且通過dispatch_semaphore_signal函數(shù)發(fā)送信號(hào)量(使信號(hào)量的值加1)械筛,dispatch_semaphore_wait函數(shù)收到信號(hào)量之后判斷信號(hào)量的值大于等于1,會(huì)再對(duì)信號(hào)量的值減1飒炎,然后當(dāng)前任務(wù)可以執(zhí)行埋哟,執(zhí)行完畢當(dāng)前任務(wù)后,再通過dispatch_semaphore_signal函數(shù)發(fā)送信號(hào)量(使信號(hào)量的值加1)郎汪,通知執(zhí)行下一個(gè)任務(wù)......如此一來(lái)赤赊,通過信號(hào)量,就達(dá)到了并發(fā)隊(duì)列中的任務(wù)同步執(zhí)行的要求煞赢。

dispatch_semaphore_signal底層實(shí)現(xiàn)過程

dispatch_semaphore_signal的內(nèi)部實(shí)現(xiàn)是

dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
    long value = os_atomic_inc2o(dsema, dsema_value, release);
    if (likely(value > 0)) {
        return 0;
    }
    if (unlikely(value == LONG_MIN)) {
        DISPATCH_CLIENT_CRASH(value,
                "Unbalanced call to dispatch_semaphore_signal()");
    }
    return _dispatch_semaphore_signal_slow(dsema);
}

os_atomic_inc2o(dsema, dsema_value, release);起底層定義是

#define os_atomic_inc2o(p, f, m) \
        os_atomic_add2o(p, f, 1, m)

也就是調(diào)用了os_atomic_add2o的函數(shù)封裝抛计;其內(nèi)部實(shí)現(xiàn)如下

#define os_atomic_add2o(p, f, v, m) \
        os_atomic_add(&(p)->f, (v), m)

也就是os_atomic_add 其內(nèi)部定義封裝是

#define os_atomic_add(p, v, m) \
        _os_atomic_c11_op((p), (v), m, add, +)

再次到_os_atomic_c11_op函數(shù)的實(shí)現(xiàn),定義如下

圖片.png

最終我們知道其內(nèi)部是調(diào)用了atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p) 而其中##o##是形參照筑,替換成第三個(gè)參數(shù)就是add得到的結(jié)果就是atomic_fetch_add_explicit,我們百度搜索就知道

原子替換指向的值obj和添加arg到舊值的結(jié)果obj吹截,并返回obj先前保存的值。操作是讀取 - 修改 - 寫入操作凝危。第一個(gè)版本根據(jù)命令對(duì)內(nèi)存進(jìn)行訪問memory_order_seq_cst饭弓,第二個(gè)版本根據(jù)內(nèi)存訪問內(nèi)存訪問order 具體鏈接

同理,dispatch_semaphore_wait也是一樣的原理過程媒抠。這就是信號(hào)量的底層實(shí)現(xiàn)邏輯。

3.4 dispatch_once_t(單利)的底層實(shí)現(xiàn)和邏輯

單利的底層實(shí)現(xiàn)代碼定義如下


dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
    dispatch_once_gate_t l = (dispatch_once_gate_t)val;

#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
    uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
    if (likely(v == DLOCK_ONCE_DONE)) {
        return;
    }
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
    if (likely(DISPATCH_ONCE_IS_GEN(v))) {
        return _dispatch_once_mark_done_if_quiesced(l, v);
    }
#endif
#endif
    if (_dispatch_once_gate_tryenter(l)) {
        return _dispatch_once_callout(l, ctxt, func);
    }
    return _dispatch_once_wait(l);
}
單利的保證線程安全過程

根據(jù)以上代碼可知咏花,

  • 1 我們可以知道外部傳入的一個(gè)任務(wù)值val趴生,單利內(nèi)部會(huì)對(duì)這個(gè)任務(wù)值進(jìn)行一個(gè)處理阀趴,處理成一個(gè)dispatch_once_gate_t對(duì)象l,
  • 2 通過內(nèi)核加載過程,看看當(dāng)前任務(wù)十分已經(jīng)被標(biāo)記過是需要執(zhí)行的任務(wù)苍匆;
  • 3 如果是被標(biāo)記過的任務(wù)就直接執(zhí)行當(dāng)前任務(wù)刘急,并且對(duì)當(dāng)前任務(wù)的執(zhí)行流程加入一個(gè)鎖操作;
  • 4 如果沒有標(biāo)記過浸踩,就對(duì)改任務(wù)進(jìn)行標(biāo)記叔汁,并且返回一個(gè)標(biāo)識(shí);
  • 5 最后通過CPU調(diào)度检碗,選擇已經(jīng)標(biāo)識(shí)過的任務(wù)進(jìn)行執(zhí)行_dispatch_once_callout
  • 6 同時(shí)有多個(gè)任務(wù)來(lái)就需要等待當(dāng)前任務(wù)的處理据块,等到處理過后進(jìn)行標(biāo)識(shí)才能從新調(diào)用;

四折剃、總結(jié)

以上就是GCD相關(guān)函數(shù)的底層實(shí)現(xiàn)過程以及一些原理的介紹另假,僅僅憑借個(gè)人的學(xué)習(xí)經(jīng)驗(yàn)和代碼閱讀過程以及Demo的調(diào)試得到的感觸,所以很多東西只是個(gè)人的理解怕犁,如果大神們有更好的思路和意見边篮,歡迎多多指正。我一定虛心學(xué)習(xí)奏甫,追求大神們的步伐戈轿。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市阵子,隨后出現(xiàn)的幾起案子思杯,更是在濱河造成了極大的恐慌,老刑警劉巖款筑,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件智蝠,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡奈梳,警方通過查閱死者的電腦和手機(jī)杈湾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)攘须,“玉大人漆撞,你說(shuō)我怎么就攤上這事∮谥妫” “怎么了浮驳?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)捞魁。 經(jīng)常有香客問我至会,道長(zhǎng),這世上最難降的妖魔是什么谱俭? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任奉件,我火速辦了婚禮宵蛀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘县貌。我一直安慰自己术陶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布煤痕。 她就那樣靜靜地躺著梧宫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪摆碉。 梳的紋絲不亂的頭發(fā)上塘匣,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音兆解,去河邊找鬼馆铁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛锅睛,可吹牛的內(nèi)容都是我干的埠巨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼现拒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼辣垒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起印蔬,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤勋桶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后侥猬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盆驹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躯喇。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蔑匣,到底是詐尸還是另有隱情棕诵,我是刑警寧澤价脾,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站秋柄,受9級(jí)特大地震影響骇笔,放射性物質(zhì)發(fā)生泄漏笨触。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望稍味。 院中可真熱鬧,春花似錦掂碱、人聲如沸疼燥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)剥槐。三九已至粒竖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挨摸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人券坞。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓恨锚,卻偏偏與公主長(zhǎng)得像猴伶,于是被迫代替她去往敵國(guó)和親他挎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子筹淫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353