dispatch_once 詳解

dispatch_once 是線程安全的

dispatch_once.png

首次調(diào)用dispatch_once時悍汛,因為外部傳入的dispatch_once_t變量值為nil宣旱,故vval會為NULL费尽,故if判斷成立赠群。然后調(diào)用_dispatch_client_callout執(zhí)行block,然后在block執(zhí)行完成之后將vval的值更新成DISPATCH_ONCE_DONE表示任務(wù)已完成旱幼。最后遍歷鏈表的節(jié)點并調(diào)用_dispatch_thread_semaphore_signal來喚醒等待中的信號量查描;

當(dāng)其他線程同時也調(diào)用dispatch_once時,因為if判斷是原子性操作柏卤,故只有一個線程進(jìn)入到if分支中冬三,其他線程會進(jìn)入else分支。在else分支中會判斷block是否已完成缘缚,如果已完成則跳出循環(huán)勾笆;否則就是更新鏈表并調(diào)用_dispatch_thread_semaphore_wait阻塞線程,等待if分支中的block完成后再喚醒當(dāng)前等待的線程桥滨。

dispatch_once用原子性操作block執(zhí)行完成標(biāo)記位匠襟,同時用信號量確保只有一個線程執(zhí)行block,等block執(zhí)行完再喚醒所有等待中的線程该园。

dispatch_once常被用于創(chuàng)建單例酸舍、swizzeld method等功能。

struct _dispatch_once_waiter_s {
volatile struct _dispatch_once_waiter_s *volatile dow_next;//鏈表下一個節(jié)點
_dispatch_thread_semaphore_t dow_sema;// 信號量
};

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

void dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}

void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
// volatileg關(guān)鍵字編輯的變量vval里初,告訴編譯器此指針指向的值隨時可能被其他線程改變啃勉,從而使得編譯器不對此指針進(jìn)行代碼編譯優(yōu)化。
// 指針的最大的作用就是間接的改變變量的值
struct _dispatch_once_waiter_s * volatile *vval = (struct _dispatch_once_waiter_s **)val;

// 初始化一個結(jié)構(gòu)體
struct _dispatch_once_waiter_s dow = {NULL, 0};

// 聲明輔助變量
struct _dispatch_once_waiter_s *tail, *tmp;

// 聲明信號變量
// uintptr_t sema
_dispatch_thread_semaphore_t sema;

// 內(nèi)置函數(shù) 原子比較交換函數(shù) __sync_bool_compare_and_swap
// 判斷vval與NULL是否相等双妨,如果相等就返回YES淮阐,并將&dow的值賦給vval
// 當(dāng)dispatch_once第一次執(zhí)行時,predicate也即val為0,地址并不為NULL刁品,但是將0轉(zhuǎn)成鏈表的時候vval為NULL泣特,那么此“原子比較交換函數(shù)”將返回YES并將vval指向值賦值為&dow,即為“等待中”挑随,_dispatch_client_callout其內(nèi)部做了一些判定状您,但實際上是調(diào)用了func而已。到此兜挨,block中的用戶代碼執(zhí)行完畢膏孟。
// #1
if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) {
    dispatch_atomic_acquire_barrier();//這是一個空的宏函數(shù),大概是注釋的作用吧
    
    // 其實質(zhì)是執(zhí)行block
    _dispatch_client_callout(ctxt, func);
    
    // cpuid指令等待拌汇,使得其他線程的【讀取到未初始化值的】預(yù)執(zhí)行能被判定為猜測未命中柒桑,從而使得這些線程能夠進(jìn)入dispatch_once_f里的另一個分支從而進(jìn)行等待
    dispatch_atomic_maximally_synchronizing_barrier();
    
    dispatch_atomic_release_barrier(); //這是一個空的宏函數(shù),大概是注釋的作用吧
    
    // dispatch_atomic_xchg 其將第二個參數(shù)的值賦給第一個參數(shù)(解引用指針)噪舀,然后返回第一個參數(shù)被賦值前的解引用值:
    // vval = &dow;
    // old = vval;
    // vval = DISPATCH_ONCE_DONE;// 置block完成標(biāo)記魁淳,是置成NULL嗎
    // tmp = old;
    tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
    
    tail = &dow;
    
    // tmp = 舊的vval = dow
    // vval = dow;
    // 接下來是對信號量鏈的處理:
    // 1.在block執(zhí)行過程中飘诗,沒有其他線程進(jìn)入本函數(shù)來等待,則vval指向值保持為&dow界逛,即tmp被賦值為&dow昆稿,即下方while循環(huán)不會被執(zhí)行,此分支結(jié)束仇奶。
    // 2.在block執(zhí)行過程中,有其他線程進(jìn)入本函數(shù)來等待進(jìn)入另一個分支比驻,那么會構(gòu)造一個信號量鏈表(vval指向值變?yōu)樾盘柫挎湹念^部该溯,鏈表的尾部為&dow),此時就會當(dāng)前分支進(jìn)入while循環(huán)别惦,在此while循環(huán)中狈茉,遍歷鏈表,逐個signal每個信號量掸掸,然后結(jié)束循環(huán)氯庆。
    while (tail != tmp) {
        while (!tmp->dow_next) {
            // 此句是為了提示cpu減少額外處理,提升性能扰付,節(jié)省電力堤撵。
            _dispatch_hardware_pause();
        }
        sema = tmp->dow_sema;
        tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
        _dispatch_thread_semaphore_signal(sema);
    }
} else {
    // #2
    // 當(dāng)執(zhí)行block分支#1未完成,且有線程再進(jìn)入本函數(shù)時羽莺,將進(jìn)入線程等待分支:
    // 先調(diào)用_dispatch_get_thread_semaphore創(chuàng)建一個信號量实昨,此信號量被賦值給dow.dow_sema。
    // 然后進(jìn)入一個無限for循環(huán)盐固,假如發(fā)現(xiàn)vval的指向值已經(jīng)為DISPATCH_ONCE_DONE荒给,即“完成”,則直接break刁卜,然后調(diào)用_dispatch_put_thread_semaphore函數(shù)銷毀信號量并退出函數(shù)
    // _dispatch_get_thread_semaphore內(nèi)部使用的是“有即取用志电,無即創(chuàng)建”策略來獲取信號量。
    dow.dow_sema = _dispatch_get_thread_semaphore();
    
    // 然后進(jìn)入一個無限for循環(huán)
    for (;;) {
        tmp = *vval;
        // 假如發(fā)現(xiàn)vval的指向值已經(jīng)為DISPATCH_ONCE_DONE蛔趴,即“完成”挑辆,則直接break
        // 然后調(diào)用_dispatch_put_thread_semaphore函數(shù)銷毀信號量并退出函數(shù)。
        if (tmp == DISPATCH_ONCE_DONE) {
            break;
        }
        dispatch_atomic_store_barrier();// 注釋作用
        
        /*
         假如vval的解引用值并非DISPATCH_ONCE_DONE孝情,則進(jìn)行一個“原子比較并交換”操作(此操作可以避免兩個等待線程同時操作鏈表帶來的問題)
         假如此時vval指向值已不再是tmp(這種情況發(fā)生在多個線程同時進(jìn)入線程等待分支#2之拨,并交錯修改鏈表)則for循環(huán)重新開始,再嘗試重新獲取一次vval來進(jìn)行同樣的操作咧叭;若指向值還是tmp蚀乔,則將vval的指向值賦值為&dow,此時val->dow_next值為NULL菲茬,可能會使得block執(zhí)行分支#1進(jìn)行while等待(如前述)吉挣,緊接著執(zhí)行dow.dow_next = tmp這句來增加鏈表節(jié)點(同時也使得block執(zhí)行分支#1的while等待結(jié)束)派撕,然后等待在信號量上,當(dāng)block執(zhí)行分支#1完成并遍歷鏈表來signal時睬魂,喚醒终吼、釋放信號量,然后一切就完成了氯哮。
         */
        // 此操作可以避免兩個等待線程同時操作鏈表帶來的問題
        // 判斷vval與tmp是否相等际跪,如果相等就返回YES,并將&dow的值賦給vval
        if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) {
            dow.dow_next = tmp;
            _dispatch_thread_semaphore_wait(dow.dow_sema);
        }
    }
    // _dispatch_put_thread_semaphore內(nèi)部使用的是“銷毀舊的喉钢,存儲新的”策略來緩存信號量
    _dispatch_put_thread_semaphore(dow.dow_sema);
}

}

深入淺出GCD 之dispatch_once

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姆打,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肠虽,更是在濱河造成了極大的恐慌幔戏,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件税课,死亡現(xiàn)場離奇詭異闲延,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)韩玩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門垒玲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人找颓,你說我怎么就攤上這事侍匙。” “怎么了?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長诲宇。 經(jīng)常有香客問我,道長说莫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任寞焙,我火速辦了婚禮储狭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捣郊。我一直安慰自己辽狈,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布呛牲。 她就那樣靜靜地躺著刮萌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪娘扩。 梳的紋絲不亂的頭發(fā)上着茸,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天壮锻,我揣著相機(jī)與錄音,去河邊找鬼涮阔。 笑死猜绣,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的敬特。 我是一名探鬼主播掰邢,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伟阔!你這毒婦竟也來了辣之?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤减俏,失蹤者是張志新(化名)和其女友劉穎召烂,沒想到半個月后碱工,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娃承,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年怕篷,在試婚紗的時候發(fā)現(xiàn)自己被綠了历筝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡廊谓,死狀恐怖梳猪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蒸痹,我是刑警寧澤春弥,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站叠荠,受9級特大地震影響匿沛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜榛鼎,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一逃呼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧者娱,春花似錦抡笼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至框沟,卻和暖如春拾碌,著一層夾襖步出監(jiān)牢的瞬間吐葱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工校翔, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留弟跑,地道東北人。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓防症,卻偏偏與公主長得像孟辑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蔫敲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,652評論 2 354

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