單例濫用 - dispatch_once死鎖造成crash(dispatch_once源碼分析)

起因

這周開發(fā)過程中遇到一個奇怪的現(xiàn)象,即在某個頁面一直卡住停留腹缩,造成卡死。而又沒有立即崩潰空扎,等待一會兒后crash了藏鹊,當即猜想是陷入了死鎖或死循環(huán)里,于是開始排查转锈,最終發(fā)現(xiàn)是由于dispatch_once濫用導致死鎖盘寡。由于項目代碼過于復雜,現(xiàn)寫了個demo總結。

demo

1岸军、創(chuàng)建兩個單例(dispatch_once方式)

NSString *const ManagerOneRefreshNotification = @"ManagerOneRefreshNotification";

@implementation ManagerOne

+ (ManagerOne *)shareInstance {
    static ManagerOne *shareInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[ManagerOne alloc] init];
    });
    return shareInstance;
}

- (instancetype)init {
    if (self = [super init]) {
        self.unReadCount = 1;
        [[NSNotificationCenter defaultCenter] postNotificationName:ManagerOneRefreshNotification object:nil];
    }
    return self;
}
@implementation ManagerTwo

+ (ManagerTwo *)shareInstance {
    static ManagerTwo *shareInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[ManagerTwo alloc] init];
    });
    return shareInstance;
}

- (instancetype)init {
    if (self = [super init]) {
        self.unReadCount = 2;
        [[NSNotificationCenter defaultCenter] postNotificationName:ManagerTwoRefreshNotification object:nil];
    }
    return self;
}

2褪测、在他們初始化后都會利用通知回調(diào)給viewcontroller進行刷新

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refresh) name:ManagerOneRefreshNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refresh) name:ManagerTwoRefreshNotification object:nil];
    [ManagerOne shareInstance];
}

- (void)refresh {
    NSLog(@"unReadCount:%d", [ManagerOne shareInstance].unReadCount + [ManagerTwo shareInstance].unReadCount);
}

3、然后就會crash


dispatch_once_crash.png

分析

從左邊的調(diào)用棧中多次出現(xiàn)dispatch_onceshareInstance可以看出是進入了死循環(huán)影涉。根據(jù)調(diào)用棧中出現(xiàn)的_dispatch_client_callout以及_dispatch_gate_wait_slow猜想可能是dispatch_once_f函數(shù)造成了信號量的永久等待,代碼更正思路好做规伐,但是為何會造成死鎖呢蟹倾?帶著疑問從dispatch_once的源碼里尋找答案。

dispatch_once源碼

Apple對于dispatch_once的源碼地址

#include "internal.h"

#undef dispatch_once
#undef dispatch_once_f


typedef struct _dispatch_once_waiter_s {
    volatile struct _dispatch_once_waiter_s *volatile dow_next;
    dispatch_thread_event_s dow_event;
    mach_port_t dow_thread;
} *_dispatch_once_waiter_t;

#define DISPATCH_ONCE_DONE ((_dispatch_once_waiter_t)~0l)

#ifdef __BLOCKS__
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{//第一步:我們調(diào)用dispatch_once入口猖闪,接下來去看最下面dispatch_once_f的定義
    dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
#endif

#if DISPATCH_ONCE_INLINE_FASTPATH
#define DISPATCH_ONCE_SLOW_INLINE inline DISPATCH_ALWAYS_INLINE
#else
#define DISPATCH_ONCE_SLOW_INLINE DISPATCH_NOINLINE
#endif 

DISPATCH_ONCE_SLOW_INLINE
static void
dispatch_once_f_slow(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
#if DISPATCH_GATE_USE_FOR_DISPATCH_ONCE
    dispatch_once_gate_t l = (dispatch_once_gate_t)val;

    if (_dispatch_once_gate_tryenter(l)) {
        _dispatch_client_callout(ctxt, func);
        _dispatch_once_gate_broadcast(l);
    } else {
        _dispatch_once_gate_wait(l);
    }
#else//第三步:主要的流程(為什么走#else請看注解二)
    _dispatch_once_waiter_t volatile *vval = (_dispatch_once_waiter_t*)val;
    struct _dispatch_once_waiter_s dow = { };
    _dispatch_once_waiter_t tail = &dow, next, tmp;
    dispatch_thread_event_t event;

//首次更改請求
    if (os_atomic_cmpxchg(vval, NULL, tail, acquire)) {
        dow.dow_thread = _dispatch_tid_self();
         //調(diào)用dispatch_once內(nèi)block回調(diào)
        _dispatch_client_callout(ctxt, func);
         //利用while循環(huán)不斷處理未完成的更改請求鲜棠,直到所有更改結束
        next = (_dispatch_once_waiter_t)_dispatch_once_xchg_done(val);
        while (next != tail) {
            tmp = (_dispatch_once_waiter_t)_dispatch_wait_until(next->dow_next);
            event = &next->dow_event;
            next = tmp;
            _dispatch_thread_event_signal(event);
        }
    } else {//非首次更改請求
        _dispatch_thread_event_init(&dow.dow_event);
        next = *vval;
        for (;;) {
//遍歷每一個后續(xù)請求,如果狀態(tài)已經(jīng)是Done培慌,直接進行下一個豁陆,同時該狀態(tài)檢測還用于避免在后續(xù)wait之前,信號量已經(jīng)發(fā)出(signal)造成的死鎖
            if (next == DISPATCH_ONCE_DONE) {
                break;
            }
//如果當前dispatch_once執(zhí)行的block沒有結束吵护,那么就將這些后續(xù)請求添加到鏈表當中
            if (os_atomic_cmpxchgv(vval, next, tail, &next, release)) {
                dow.dow_thread = next->dow_thread;
                dow.dow_next = next;
                if (dow.dow_thread) {
                    pthread_priority_t pp = _dispatch_get_priority();
                    _dispatch_thread_override_start(dow.dow_thread, pp, val);
                }
                _dispatch_thread_event_wait(&dow.dow_event);
                if (dow.dow_thread) {
                    _dispatch_thread_override_end(dow.dow_thread, val);
                }
                break;
            }
        }
        _dispatch_thread_event_destroy(&dow.dow_event);
    }
#endif
}

DISPATCH_NOINLINE
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
#if !DISPATCH_ONCE_INLINE_FASTPATH
    if (likely(os_atomic_load(val, acquire) == DLOCK_ONCE_DONE)) {
        return;
    }
#endif //第二步:進入dispatch_once_f_slow(這個宏判斷請看注解一)
    return dispatch_once_f_slow(val, ctxt, func);
}
注解一:

DISPATCH_ONCE_INLINE_FASTPATH這個宏的值由CPU架構決定盒音,__x86_64__(64位),__i386__(32位)馅而,__s390x__(運行在IBM z系統(tǒng)(s390x)祥诽,可能Apple和IBM比較熟,給他留后門了)用爪,以及__APPLE__這個就無從得知了原押,可能是Apple自身的平臺架構,這些情況下DISPATCH_ONCE_INLINE_FASTPATH = 1偎血,所以大部分情況也就是1了诸衔。

#if defined(__x86_64__) || defined(__i386__) || defined(__s390x__)
#define DISPATCH_ONCE_INLINE_FASTPATH 1
#elif defined(__APPLE__)
#define DISPATCH_ONCE_INLINE_FASTPATH 1
#else
#define DISPATCH_ONCE_INLINE_FASTPATH 0
#endif
注解二:

DISPATCH_GATE_USE_FOR_DISPATCH_ONCE這個宏的值在lock.h中有定義:

#pragma mark - gate lock

#if HAVE_UL_UNFAIR_LOCK || HAVE_FUTEX
#define DISPATCH_GATE_USE_FOR_DISPATCH_ONCE 1
#else
#define DISPATCH_GATE_USE_FOR_DISPATCH_ONCE 0
#endif

HAVE_UL_UNFAIR_LOCK的值和HAVE_FUTEX的值也在lock.h中有定義:

#ifdef __linux__
#define HAVE_FUTEX 1
#else
#define HAVE_FUTEX 0
#endif
#ifdef UL_UNFAIR_LOCK
#define HAVE_UL_UNFAIR_LOCK 1
#endif

從上面的分析可以看出:
1盯漂、dispatch_once不止是簡單的執(zhí)行一次,如果再次調(diào)用會進入非首次更改的模塊笨农,如果有未DONE的請求會被添加到鏈表中
2就缆、所以dispatch_once本質(zhì)上可以接受多次請求,會對此維護一個請求鏈表
3谒亦、如果在block執(zhí)行期間竭宰,多次進入調(diào)用同類的dispatch_once函數(shù)(即單例函數(shù)),會導致整體鏈表無限增長份招,造成永久性死鎖
4切揭、對于開始問題大致上和 A -> B -> A的流程類似,理解dispatch_once的內(nèi)部流程有利于在使用中規(guī)避隱藏的問題锁摔。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末廓旬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谐腰,更是在濱河造成了極大的恐慌孕豹,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件十气,死亡現(xiàn)場離奇詭異励背,居然都是意外死亡,警方通過查閱死者的電腦和手機砸西,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門叶眉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人籍胯,你說我怎么就攤上這事竟闪。” “怎么了杖狼?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長妖爷。 經(jīng)常有香客問我蝶涩,道長,這世上最難降的妖魔是什么絮识? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任绿聘,我火速辦了婚禮,結果婚禮上次舌,老公的妹妹穿的比我還像新娘熄攘。我一直安慰自己,他們只是感情好彼念,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布挪圾。 她就那樣靜靜地躺著浅萧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哲思。 梳的紋絲不亂的頭發(fā)上洼畅,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音棚赔,去河邊找鬼帝簇。 笑死,一個胖子當著我的面吹牛靠益,可吹牛的內(nèi)容都是我干的丧肴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼胧后,長吁一口氣:“原來是場噩夢啊……” “哼芋浮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起绩卤,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤途样,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后濒憋,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體何暇,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年凛驮,在試婚紗的時候發(fā)現(xiàn)自己被綠了裆站。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡黔夭,死狀恐怖宏胯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情本姥,我是刑警寧澤肩袍,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站婚惫,受9級特大地震影響氛赐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜先舷,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一艰管、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蒋川,春花似錦牲芋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夕冲。三九已至,卻和暖如春餐济,著一層夾襖步出監(jiān)牢的瞬間耘擂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工絮姆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留醉冤,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓篙悯,卻偏偏與公主長得像蚁阳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鸽照,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

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