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

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

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

帶著問題,我們還是先看dispatch_once_f這個函數(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.局部變量块饺,用于在遍歷鏈表過程中獲取每一個在鏈表上的更改請求的信號量
    _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之后雌芽,會調(diào)用dispatch_atomic_maximally_synchronizing_barrier()
        //宏函數(shù)授艰,在intel處理器上,這個函數(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. 如果這個時(shí)候tmp的next指針還沒更新完畢,就等待一會屉佳,提示cpu減少額外處理谷朝,提升性能,節(jié)省電力武花。
            while (!tmp->dow_next) 
            {
                _dispatch_hardware_pause();
            }

            // 11. 取出當(dāng)前的信號量圆凰,告訴等待者,這次更改請求完成了体箕,輪到下一個了
            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. 遍歷每一個后續(xù)請求,如果狀態(tài)已經(jīng)是Done累铅,直接進(jìn)行下一個
        // 同時(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)識幾個對象.

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有個印象娃兽,因?yàn)楹竺鏁谩?/p>

**1.dispatch_once_f(dispatch_once_t val, void ctxt, dispatch_function_t func)傳入了三個參數(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的另外一個分支(else分支)進(jìn)行等待。
5.tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);使其為DISPATCH_ONCE_DONE冲泥,即“完成”。
6.然后比較 tmp和&dow的值,如果這兩個相等凡恍,分支結(jié)束志秃。
7.如果 tmp和&dow的值不相等,為什么會不相等呢嚼酝。因?yàn)樵赽lock執(zhí)行過程中浮还,會有其他線程進(jìn)入到本函數(shù),我們可以看else后面的內(nèi)容闽巩,會形成一個信號量鏈表钧舌,(vval指向值變?yōu)樾盘柫挎湹念^部,鏈表的尾部為&dow)涎跨,在這時(shí)候洼冻,進(jìn)入分支1的while循環(huán)中,因?yàn)槲覀兦懊嬗绾埽瑂truct _dispatch_once_waiter_s dow = { NULL, 0 }; 撞牢,dow.dow_next為null,所以需要一直等待叔营,等待temp.dow_next有值才可以進(jìn)行后面的操作屋彪。然后分支1就會進(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)建了一個信號量婴谱,并把值賦值給dow.dow_sema.

 dow.dow_sema = _dispatch_get_thread_semaphore();

然后進(jìn)入了一個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)部會構(gòu)建一個倆表來維護(hù),如果在block完成之前夜矗,有其它的調(diào)用者進(jìn)來泛范,則會把這些調(diào)用者放到一個waiter鏈表中。
waiter鏈表中的每個調(diào)用者會等待一個信號量(dow.dow_sema)紊撕。在block執(zhí)行完了后罢荡,除了將onceToken置為DISPATCH_ONCE_DONE外,還會去遍歷waiter鏈中的所有waiter对扶,拋出相應(yīng)的信號量区赵,以告知waiter們調(diào)用已經(jīng)結(jié)束了

上面的兩個問題。

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

濫用單例的為什么會死鎖各谚。
如果在dispatch_once函數(shù)的block塊執(zhí)行期間,循環(huán)進(jìn)入自己的dispatch_once函數(shù)到千,會造成鏈表一直增長昌渤,同樣也會造成死鎖。(這里只是簡單的A->B->A->B->A這樣的循環(huán)憔四,也可以是A->A->A這樣的更加直接的循環(huán).
如果在block執(zhí)行期間膀息,多次進(jìn)入調(diào)用同類的dispatch_once函數(shù)(即單例函數(shù)),會導(dǎo)致整體鏈表無限增長了赵,造成永久性死鎖
我覺得這也就是之前潜支,坐那個直播中,用信號量來控制時(shí)柿汛,為什么會卡主冗酿,因?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)錯位置,在對應(yīng)著源碼貌笨,應(yīng)該可以看出問題所在弱判。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锥惋,隨后出現(xiàn)的幾起案子昌腰,更是在濱河造成了極大的恐慌开伏,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剥哑,死亡現(xiàn)場離奇詭異硅则,居然都是意外死亡淹父,警方通過查閱死者的電腦和手機(jī)株婴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來暑认,“玉大人困介,你說我怎么就攤上這事≌杭剩” “怎么了座哩?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長粮彤。 經(jīng)常有香客問我根穷,道長,這世上最難降的妖魔是什么导坟? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任屿良,我火速辦了婚禮,結(jié)果婚禮上惫周,老公的妹妹穿的比我還像新娘尘惧。我一直安慰自己,他們只是感情好递递,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布喷橙。 她就那樣靜靜地躺著,像睡著了一般登舞。 火紅的嫁衣襯著肌膚如雪贰逾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天菠秒,我揣著相機(jī)與錄音疙剑,去河邊找鬼。 笑死稽煤,一個胖子當(dāng)著我的面吹牛核芽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播酵熙,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼轧简,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了匾二?” 一聲冷哼從身側(cè)響起哮独,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤拳芙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后皮璧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舟扎,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年悴务,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了睹限。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡讯檐,死狀恐怖羡疗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情别洪,我是刑警寧澤叨恨,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站挖垛,受9級特大地震影響痒钝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痢毒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一送矩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧闸准,春花似錦益愈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至库快,卻和暖如春摸袁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背义屏。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工靠汁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人闽铐。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓蝶怔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兄墅。 傳聞我的和親對象是個殘疾皇子踢星,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

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