『ios』dispatch_once死鎖和濫用單例導(dǎo)致的問題

在學(xué)習(xí)dispatch_once原理過程中,發(fā)現(xiàn)了之前因?yàn)樾盘柫恳鸬目ㄗ≈骶€程的問題所在。
所以,了解原理,絕對是提高自己的必備條件。

我們帶著兩個(gè)問題去看
1.單例為什么會(huì)造成死鎖。
2.濫用單例為什么會(huì)導(dǎo)致內(nèi)存不斷增加。
如果對dispatch_once的基礎(chǔ)原理還不了解,可以看上一篇文章。

帶著問題,我們還是先看dispatch_once_f這個(gè)函數(shù)。

#include "internal.h"

#undef dispatch_once
#undef dispatch_once_f

struct _dispatch_once_waiter_s 
{
    volatile struct _dispatch_once_waiter_s *volatile dow_next;
    _dispatch_thread_semaphore_t dow_sema;
};

#define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)

#ifdef __BLOCKS__

// 1.應(yīng)用程序調(diào)用的入口
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
    struct Block_basic *bb = (void *)block;

    // 2. 內(nèi)部邏輯
    dispatch_once_f(val, block, (void *)bb->Block_invoke);
}
#endif

DISPATCH_NOINLINE
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
    struct _dispatch_once_waiter_s * volatile *vval =
        (struct _dispatch_once_waiter_s**)val;

    // 3. 類似于簡單的哨兵位
    struct _dispatch_once_waiter_s dow = { NULL, 0 };

    // 4. 在Dispatch_Once的block執(zhí)行期進(jìn)入的dispatch_once_t更改請求的鏈表
    struct _dispatch_once_waiter_s *tail, *tmp;

    // 5.局部變量,用于在遍歷鏈表過程中獲取每一個(gè)在鏈表上的更改請求的信號量
    _dispatch_thread_semaphore_t sema;

    // 6. Compare and Swap(用于首次更改請求)
    if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) 
    {
        dispatch_atomic_acquire_barrier();

        // 7.調(diào)用dispatch_once的block
        _dispatch_client_callout(ctxt, func);

        //在寫入端,dispatch_once在執(zhí)行了block之后,會(huì)調(diào)用dispatch_atomic_maximally_synchronizing_barrier()
        //宏函數(shù),在intel處理器上,這個(gè)函數(shù)編譯出的是cpuid指令。

        dispatch_atomic_maximally_synchronizing_barrier();

        //dispatch_atomic_release_barrier(); // assumed contained in above

        // 8. 更改請求成為DISPATCH_ONCE_DONE(原子性的操作)
        tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);

        tail = &dow;

        // 9. 發(fā)現(xiàn)還有更改請求,繼續(xù)遍歷
        while (tail != tmp) 
        {
            // 10. 如果這個(gè)時(shí)候tmp的next指針還沒更新完畢,就等待一會(huì),提示cpu減少額外處理,提升性能,節(jié)省電力。
            while (!tmp->dow_next) 
            {
                _dispatch_hardware_pause();
            }

            // 11. 取出當(dāng)前的信號量,告訴等待者,這次更改請求完成了,輪到下一個(gè)了
            sema = tmp->dow_sema;

            tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;

            _dispatch_thread_semaphore_signal(sema);
        }
    } else 
    {    
        // 12. 非首次請求,進(jìn)入此邏輯塊
        dow.dow_sema = _dispatch_get_thread_semaphore();

        // 13. 遍歷每一個(gè)后續(xù)請求,如果狀態(tài)已經(jīng)是Done,直接進(jìn)行下一個(gè)
        // 同時(shí)該狀態(tài)檢測還用于避免在后續(xù)wait之前,信號量已經(jīng)發(fā)出(signal)造成
        // 的死鎖
        for (;;) 
        {
            tmp = *vval;
            if (tmp == DISPATCH_ONCE_DONE) 
            {
                break;
            }
            dispatch_atomic_store_barrier();

            // 14. 如果當(dāng)前dispatch_once執(zhí)行的block沒有結(jié)束,那么就將這些
            // 后續(xù)請求添加到鏈表當(dāng)中
            if (dispatch_atomic_cmpxchg(vval, tmp, &dow))
            {
                dow.dow_next = tmp;
                _dispatch_thread_semaphore_wait(dow.dow_sema);
            }
        }
        _dispatch_put_thread_semaphore(dow.dow_sema);
    }
}

首先我們先來認(rèn)識幾個(gè)對象.

struct _dispatch_once_waiter_s 
{
    volatile struct _dispatch_once_waiter_s *volatile dow_next;
    _dispatch_thread_semaphore_t dow_sema;
};
 struct _dispatch_once_waiter_s dow = { NULL, 0 }; 

要對dow.dow_next有個(gè)印象,因?yàn)楹竺鏁?huì)用。

**1.dispatch_once_f(dispatch_once_t val, void ctxt, dispatch_function_t func)傳入了三個(gè)參數(shù)ctxt是外部傳入的block的指針,func是block里具體執(zhí)行的函數(shù)。
2. dispatch_atomic_cmpxchg 是原子交換函數(shù),dispatch_atomic_cmpxchg(vval, NULL, &dow)也就是吧vval的值賦值給&dow.
3. _dispatch_client_callout(ctxt, func);根據(jù)ctxt找到block,并執(zhí)行block中的函數(shù)。
4. dispatch_atomic_maximally_synchronizing_barrier函數(shù)的作用,是可以讓其他線程來讀取到未初始化的對象,從而可以使這些線程進(jìn)入dispatch_once_f的另外一個(gè)分支(else分支)進(jìn)行等待。
5.tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);使其為DISPATCH_ONCE_DONE,即“完成”。
6.然后比較 tmp和&dow的值,如果這兩個(gè)相等,分支結(jié)束。
7.如果 tmp和&dow的值不相等,為什么會(huì)不相等呢。因?yàn)樵赽lock執(zhí)行過程中,會(huì)有其他線程進(jìn)入到本函數(shù),我們可以看else后面的內(nèi)容,會(huì)形成一個(gè)信號量鏈表,(vval指向值變?yōu)樾盘柫挎湹念^部,鏈表的尾部為&dow),在這時(shí)候,進(jìn)入分支1的while循環(huán)中,因?yàn)槲覀兦懊?,struct _dispatch_once_waiter_s dow = { NULL, 0 }; ,dow.dow_next為null,所以需要一直等待,等待temp.dow_next有值才可以進(jìn)行后面的操作。然后分支1就會(huì)進(jìn)行等待分支2的進(jìn)行,只有當(dāng)分支2的dow_dow_next = tmp被執(zhí)行了,才可以繼續(xù)往后面執(zhí)行。

while (!tmp->dow_next) 
            {
                _dispatch_hardware_pause();
            }

8.我們仔細(xì)看下分支2的操作。
創(chuàng)建了一個(gè)信號量,并把值賦值給dow.dow_sema.

 dow.dow_sema = _dispatch_get_thread_semaphore();

然后進(jìn)入了一個(gè)for循環(huán)中,如果vval的值已經(jīng)為DISPATCH_ONCE_DONE,則直接break。
如果vval的值不為DISPATCH_ONCE_DONE,則把vval賦值給&dow.此時(shí)val.dow_next還是為null,把dow.dow_next = tmp來增加鏈表的節(jié)點(diǎn),解決了分支1中while進(jìn)行等待的問題。

 if (dispatch_atomic_cmpxchg(vval, tmp, &dow))
            {
                dow.dow_next = tmp;
                _dispatch_thread_semaphore_wait(dow.dow_sema);
            }

然后等待在信號量上,當(dāng)block執(zhí)行分支1完成并遍歷鏈表來signal時(shí),喚醒、釋放信號量,然后一切就完成了。

分支1的while循環(huán),需要等待分支2的 dow.dow_next = tmp;賦值,然后,分支2的 _dispatch_thread_semaphore_wait(dow.dow_sema);需要等待分支1的_dispatch_thread_semaphore_signal(sema);。

總結(jié)下上面的問題。
dispatch_once實(shí)際上內(nèi)部會(huì)構(gòu)建一個(gè)倆表來維護(hù),如果在block完成之前,有其它的調(diào)用者進(jìn)來,則會(huì)把這些調(diào)用者放到一個(gè)waiter鏈表中。
waiter鏈表中的每個(gè)調(diào)用者會(huì)等待一個(gè)信號量(dow.dow_sema)。在block執(zhí)行完了后,除了將onceToken置為DISPATCH_ONCE_DONE外,還會(huì)去遍歷waiter鏈中的所有waiter,拋出相應(yīng)的信號量,以告知waiter們調(diào)用已經(jīng)結(jié)束了

上面的兩個(gè)問題。

死鎖如何形成?
兩個(gè)類相互調(diào)用其單例方法時(shí),調(diào)用者TestA作為一個(gè)waiter,在等待TestB中的block完成,而TestB中block的完成依賴于TestA中單例函數(shù)的block的執(zhí)行完成,而TestA中的block想要完成還需要TestB中的block完成……兩個(gè)人都在相互等待對方的完成,這就成了一個(gè)死鎖。

濫用單例的為什么會(huì)死鎖。
如果在dispatch_once函數(shù)的block塊執(zhí)行期間,循環(huán)進(jìn)入自己的dispatch_once函數(shù),會(huì)造成鏈表一直增長,同樣也會(huì)造成死鎖。(這里只是簡單的A->B->A->B->A這樣的循環(huán),也可以是A->A->A這樣的更加直接的循環(huán).
如果在block執(zhí)行期間,多次進(jìn)入調(diào)用同類的dispatch_once函數(shù)(即單例函數(shù)),會(huì)導(dǎo)致整體鏈表無限增長,造成永久性死鎖
我覺得這也就是之前,坐那個(gè)直播中,用信號量來控制時(shí),為什么會(huì)卡主,因?yàn)槲矣脝卫庋b的信號量,然后單例循環(huán)調(diào)用,發(fā)生了死鎖。

2021.8.10 補(bǔ)充一下死鎖的demo

#import "ShareA.h"
#import "ShareB.h"
@implementation ShareA

+(instancetype)instance {
    static ShareA *a;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [[ShareB instance] test];
        a = [[ShareA alloc]init];
    });
    return a;
}
- (void)test {
    NSLog(@"ShareA");
}

@end

#import "ShareB.h"
#import "ShareA.h"
@implementation ShareB

+(instancetype)instance {
    static ShareB *a;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [[ShareA instance]test];
        a = [[ShareB alloc]init];
    });
    return a;
}

- (void)test {
    NSLog(@"ShareB");
}

@end


image.png
image.png

通過下面的報(bào)錯(cuò)位置,在對應(yīng)著源碼,應(yīng)該可以看出問題所在。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容