dispatch_once 是線程安全的
首次調(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);
}
}