在開發(fā)中使用單例是最經(jīng)常不過的事兒了,最常用的就是dispatch_once這個(gè)函數(shù),這個(gè)函數(shù)可以使其參數(shù)內(nèi)的block塊只在全局執(zhí)行一次從而達(dá)到目的,不過這個(gè)函數(shù)要是用的稍微“巧”了的話,就會(huì)出問題。比如看下面這段代碼:
#import "TestA.h"
@implementation TestA
+(TestA *)shareInstanceA
{
static TestA *testa = nil;
static dispatch_once_t token;
dispatch_once(&token, ^{
testa = [[TestA alloc]init];
});
return testa;
}
-(instancetype)init
{
self = [super init];
if (self) {
[TestB shareInstanceB];
}
return self;
}
@end
@implementation TestB
+(TestB *)shareInstanceB
{
static TestB *testb = nil;
static dispatch_once_t token;
dispatch_once(&token, ^{
testb = [[TestB alloc]init];
});
return testb;
}
-(instancetype)init
{
self = [super init];
if(self)
{
[TestA shareInstanceA];
}
return self;
}
@end
在viewDidload里面創(chuàng)建TestA的對(duì)象,猜猜結(jié)果會(huì)怎樣?卡死了,直接不動(dòng)了,造成死鎖了(我等了30秒還是沒動(dòng)靜……)。這個(gè)情況曾經(jīng)在幫同學(xué)查問題的時(shí)候遇見過一次?,F(xiàn)在暫停程序,查看調(diào)用棧的狀態(tài)如下圖所示:

發(fā)現(xiàn)這么幾個(gè)函數(shù)調(diào)用的很頻繁:dispatch_once_f和shareInstanceA和shareInstanceB,而且在棧頂部的函數(shù)是semaphore_wait_trap和dispatch_once_f,程序最后是在dispatch_once_f卡死的,在這兒出現(xiàn)的問題,那么這個(gè)函數(shù)有巨大的懷疑,然后就想辦法搞到這個(gè)函數(shù)的源碼,因?yàn)镚CD部分是開源的,可以找到代碼,尋找途徑一:http://libdispatch.macosforge.org/trac/browser#tags/libdispatch-200/src ,在這個(gè)路徑下,有一個(gè)once.c的文件,里面就有此函數(shù)的代碼。路徑二:在git bash中輸入命令:git clone git://git.macosforge.org/libdispatch.git,在下載下來的文件中找到src文件夾下的once.c文件?,F(xiàn)在就看看就看看它的內(nèi)部實(shí)現(xiàn)吧,為了后面的內(nèi)容所提及的代碼,也把once.h文件的代碼放到下面了,由于這個(gè)文件的代碼不多,放到第一部分:
once.h的代碼:
#ifndef __DISPATCH_ONCE__
#define __DISPATCH_ONCE__
#ifndef __DISPATCH_INDIRECT__
#error "Please #include <dispatch/dispatch.h> instead of this file directly."
#include <dispatch/base.h> // for HeaderDoc
#endif
__BEGIN_DECLS
/*!
* @typedef dispatch_once_t
*
* @abstract
* A predicate for use with dispatch_once(). It must be initialized to zero.
* Note: static and global variables default to zero.
*/
typedef long dispatch_once_t;
/*!
* @function dispatch_once
*
* @abstract
* Execute a block once and only once.
*
* @param predicate
* A pointer to a dispatch_once_t that is used to test whether the block has
* completed or not.
*
* @param block
* The block to execute once.
*
* @discussion
* Always call dispatch_once() before using or testing any variables that are
* initialized by the block.
*/
#ifdef __BLOCKS__
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
void
dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
//注意這個(gè)內(nèi)聯(lián)函數(shù)
DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
void
_dispatch_once(dispatch_once_t *predicate, dispatch_block_t block)
{
if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
dispatch_once(predicate, block);
}
}
#undef dispatch_once
#define dispatch_once _dispatch_once
#endif
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_NONNULL1 DISPATCH_NONNULL3 DISPATCH_NOTHROW
void
dispatch_once_f(dispatch_once_t *predicate, void *context,
dispatch_function_t function);
DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_NONNULL1 DISPATCH_NONNULL3
DISPATCH_NOTHROW
void
_dispatch_once_f(dispatch_once_t *predicate, void *context,
dispatch_function_t function)
{
if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
dispatch_once_f(predicate, context, function);
}
}
#undef dispatch_once_f
#define dispatch_once_f _dispatch_once_f
__END_DECLS
#endif
現(xiàn)在要提前補(bǔ)習(xí)一點(diǎn)知識(shí)。來看上面那些代碼的大概意思:
大體意思
看那個(gè)內(nèi)聯(lián)函數(shù)的注釋,這其實(shí)是dispatch_once讀取端的實(shí)現(xiàn),對(duì)應(yīng)還有寫入端(后面有提到),DISPATCH_EXPECT函數(shù)是告訴cpu *predicate的值等于 ~0l 是很有可能的判定結(jié)果,DISPATCH_EXPECT展開就是 __builtin_expect((x), (v)),它是GCC引進(jìn)的宏,其作用就是幫助編譯器判斷條件跳轉(zhuǎn)的預(yù)期值,避免跳轉(zhuǎn)造成時(shí)間浪費(fèi)。并沒有改變其對(duì)真值的判斷。主要目的就是增加效率,降低負(fù)載,為什么要降低負(fù)載呢,原因如下。
原因
先來看dispatch_once的三個(gè)使用時(shí)的所在場(chǎng)景:
- 第一次執(zhí)行,block需要調(diào)用,調(diào)用結(jié)束后要改變標(biāo)記量
- 不是第一次執(zhí)行,并且這時(shí)候步驟1中的沒執(zhí)行完,當(dāng)前線程需要等待步驟1的完成
- 不是第一次執(zhí)行,但是這時(shí)候步驟1執(zhí)行結(jié)束,當(dāng)前線程跳過block執(zhí)行后續(xù)的任務(wù)
對(duì)于場(chǎng)景1,性能瓶頸在于block所執(zhí)行的任務(wù),而不在dispatch_once函數(shù)本身。場(chǎng)景2,發(fā)生的幾率不大,即時(shí)遇到這種情況,只需等待前面的線程執(zhí)行結(jié)束就可。場(chǎng)景3,則可能經(jīng)常遇到,前面的dispatch_once一旦執(zhí)行結(jié)束,后面所有的線程遇到了之后都是場(chǎng)景3的情況。
dispatch_once本意是只執(zhí)行一次就結(jié)束它的使命,具有一次性,一旦執(zhí)行結(jié)束后,它就沒意義了,希望后面的任務(wù)在執(zhí)行中基本不會(huì)受其影響,一旦有大的影響,就會(huì)造成性能負(fù)擔(dān),因此希望它盡可能降低對(duì)后續(xù)調(diào)用的負(fù)載。那么多大影響才算“較小”的影響,這需要一個(gè)基準(zhǔn)線,這個(gè)基準(zhǔn)線就是非線程安全的純if判斷語句,而dispatch_once確實(shí)接近這個(gè)基準(zhǔn)值。
解決辦法
那么要采取什么辦法解決這個(gè)問題呢,在翻閱一大堆外文資料、博客、翻譯后,發(fā)現(xiàn)這里面涉及到一些關(guān)于CPU的知識(shí):
cpu的分支預(yù)測(cè)和預(yù)執(zhí)行
流水線特性使得CPU能更快地執(zhí)行線性指令序列,但是當(dāng)遇到條件判斷分支時(shí),麻煩來了,在判定語句返回結(jié)果之前,cpu不知道該執(zhí)行哪個(gè)分支,那就得等著(術(shù)語叫做pipeline stall),所以,CPU會(huì)進(jìn)行預(yù)執(zhí)行推理,cpu先猜測(cè)一個(gè)可能的分支,然后開始執(zhí) 行分支中的指令?,F(xiàn)代CPU一般都能做到超過90%的猜測(cè)命中率。然后當(dāng)判定語句返回,如果cpu猜錯(cuò)分支,那么之前進(jìn)行的執(zhí)行都會(huì)被拋棄,然后從正確的分支重新開始執(zhí)行(在once.c文件里的源代碼注釋里提到之)。
在dispatch_once中,唯一一個(gè)判斷分支就是predicate,dispatch_once會(huì)讓CPU預(yù)先執(zhí)行條件不成立的分支,這樣可以大大提升函數(shù)執(zhí)行速度(在once.c文件里的源代碼注釋里提到之)。但是這樣的預(yù)執(zhí)行導(dǎo)致的結(jié)果是使用了未初始化的obj并將函數(shù)返回,這顯然不是預(yù)期結(jié)果。
不對(duì)稱barrier
編寫barrier時(shí),應(yīng)該是對(duì)稱的,在寫入端,要有一個(gè)barrier來保證順序?qū)懭耄瑫r(shí),在讀取端,也要有一個(gè)barrier來保證順序讀取。但是,我們的dispatch_once實(shí)現(xiàn)要求寫入端快不快無所謂,而讀取端盡可能的快。所以,我們要解決前述的預(yù)執(zhí)行引起的問題。
當(dāng)一個(gè)預(yù)執(zhí)行最終被發(fā)現(xiàn)是錯(cuò)誤的猜測(cè)時(shí),所有的預(yù)執(zhí)行狀態(tài)以及結(jié)果都會(huì)被清除,然后cpu會(huì)從判斷分支處重新執(zhí)行正確的分支,也就意味著被讀取的未初始化的obj也會(huì)被拋棄,然后讀取。假如dispatch_once能做到在執(zhí)行完block并正確賦值給obj后,告訴其它c(diǎn)pu核心:剛才都猜錯(cuò)了!然后cpu就會(huì)重新從分支處開始執(zhí)行,進(jìn)而獲取正確的obj值并返回。
從最早的預(yù)執(zhí)行到條件判斷語句最終結(jié)果被計(jì)算出來,這之間有很長時(shí)間(記作Ta),具體多長取決于cpu的設(shè)計(jì),但是不論如何,這個(gè)時(shí)間最多幾十圈cpu時(shí)鐘時(shí)間(在once.c文件里的源代碼注釋里提到之),假如寫入端能在【初始化并寫入obj】與【置predicate值】之間等待足夠長的時(shí)間Tb使得Tb大于等于Ta,那問題就都解決了。
如果覺得這個(gè)”解決”難以理解,那么反過來思考,假如Tb小于Ta,那么Tb就有可能被Ta完全包含,也就是說,另一個(gè)線程B(耗時(shí)為Ta)在預(yù)執(zhí)行讀取了未初始化的obj值之后,回過頭來確認(rèn)猜測(cè)正確性時(shí),predicate可能被執(zhí)行block的線程A置為了“完成”,這就導(dǎo)致線程B認(rèn)為自己的預(yù)執(zhí)行有效(實(shí)際上它讀取了未初始化的值)。而假如Tb大于等于Ta,任何讀取了未初始化的obj值的預(yù)執(zhí)行都會(huì)被判定為未命中,從而進(jìn)入內(nèi)層dispatch_once而進(jìn)行等待。
要保證足夠的等待時(shí)間,需要一些trick。在intel的CPU上,dispatch_once動(dòng)用了cpuid指令來達(dá)成這個(gè)目的。cpuid本來是用作取得cpu的信息,但是這個(gè)指令也同時(shí)強(qiáng)制將指令流串行化,并且這個(gè)指令是需要比較長的執(zhí)行時(shí)間的(在某些cpu上,甚至需要幾百圈cpu時(shí)鐘),這個(gè)時(shí)間Tb足夠超過Ta了。
接著大體意思繼續(xù)看
DISPATCH_EXPECT函數(shù)的作用是使得CPU在猜測(cè)上有更大的幾率提高正確率,猜測(cè)到正確的分支,最重要的是,這一句是個(gè)簡單的if判定語句,負(fù)載無限接近基準(zhǔn)值。到此,讀取端的介紹完畢。
這是once.c的代碼
#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更改請(qǐng)求的鏈表
struct _dispatch_once_waiter_s *tail, *tmp;
// 5.局部變量,用于在遍歷鏈表過程中獲取每一個(gè)在鏈表上的更改請(qǐng)求的信號(hào)量
_dispatch_thread_semaphore_t sema;
// 6. Compare and Swap(用于首次更改請(qǐng)求)
if (dispatch_atomic_cmpxchg(vval, NULL, &dow))
{
dispatch_atomic_acquire_barrier();
// 7.調(diào)用dispatch_once的block
_dispatch_client_callout(ctxt, func);
// The next barrier must be long and strong.
//
// The scenario: SMP systems with weakly ordered memory models
// and aggressive out-of-order instruction execution.
//
// The problem:
//
// The dispatch_once*() wrapper macro causes the callee's
// instruction stream to look like this (pseudo-RISC):
//
// load r5, pred-addr
// cmpi r5, -1
// beq 1f
// call dispatch_once*()
// 1f:
// load r6, data-addr
//
// May be re-ordered like so:
//
// load r6, data-addr
// load r5, pred-addr
// cmpi r5, -1
// beq 1f
// call dispatch_once*()
// 1f:
//
// Normally, a barrier on the read side is used to workaround
// the weakly ordered memory model. But barriers are expensive
// and we only need to synchronize once! After func(ctxt)
// completes, the predicate will be marked as "done" and the
// branch predictor will correctly skip the call to
// dispatch_once*().
//
// A far faster alternative solution: Defeat the speculative
// read-ahead of peer CPUs.
//
// Modern architectures will throw away speculative results
// once a branch mis-prediction occurs. Therefore, if we can
// ensure that the predicate is not marked as being complete
// until long after the last store by func(ctxt), then we have
// defeated the read-ahead of peer CPUs.
//
// In other words, the last "store" by func(ctxt) must complete
// and then N cycles must elapse before ~0l is stored to *val.
// The value of N is whatever is sufficient to defeat the
// read-ahead mechanism of peer CPUs.
//
// On some CPUs, the most fully synchronizing instruction might
// need to be issued.
//在寫入端,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. 更改請(qǐng)求成為DISPATCH_ONCE_DONE(原子性的操作)
tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
tail = &dow;
// 9. 發(fā)現(xiàn)還有更改請(qǐng)求,繼續(xù)遍歷
while (tail != tmp)
{
// 10. 如果這個(gè)時(shí)候tmp的next指針還沒更新完畢,就等待一會(huì),提示cpu減少額外處理,提升性能,節(jié)省電力。
while (!tmp->dow_next)
{
_dispatch_hardware_pause();
}
// 11. 取出當(dāng)前的信號(hào)量,告訴等待者,這次更改請(qǐng)求完成了,輪到下一個(gè)了
sema = tmp->dow_sema;
tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
_dispatch_thread_semaphore_signal(sema);
}
} else
{
// 12. 非首次請(qǐng)求,進(jìn)入此邏輯塊
dow.dow_sema = _dispatch_get_thread_semaphore();
// 13. 遍歷每一個(gè)后續(xù)請(qǐng)求,如果狀態(tài)已經(jīng)是Done,直接進(jìn)行下一個(gè)
// 同時(shí)該狀態(tài)檢測(cè)還用于避免在后續(xù)wait之前,信號(hào)量已經(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ù)請(qǐng)求添加到鏈表當(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);
}
}
一堆宏函數(shù)和一堆線程同步代碼,看的頭大……結(jié)合前面提到的和源代碼注釋一點(diǎn)一點(diǎn)的看:
dispatch_once函數(shù)的里面其實(shí)是調(diào)用了dispatch_once_f函數(shù),而f的意思是C函數(shù)(沒有帶f的是調(diào)用了block),但是block最終還是調(diào)用了C函數(shù)。當(dāng)調(diào)用了dispatch_once_f函數(shù)的時(shí)候,val是外部傳入的predicate,ctxt指的是外部傳入的block的指針,func是block里的具體執(zhí)行體函數(shù),執(zhí)行func就是執(zhí)行block。接著聲明了一堆變量,
vval是volatile標(biāo)記了的val,volatile修飾符的意思大概是告訴編譯器:這個(gè)指針?biāo)赶虻闹?,可能隨時(shí)會(huì)被其他線程所改變,使編譯器不再對(duì)此指針進(jìn)行代碼編譯優(yōu)化。dow意為dispatch_once_wait。dispatch_atomic_cmpxchg是“原子比較交換函數(shù)”__sync_bool_compare_and_swap的宏替換,然后進(jìn)入分支1:執(zhí)行block分支,當(dāng)dispatch_once第一次執(zhí)行時(shí),predicate也就是val的值為0,這時(shí)候“原子比較交換函數(shù)”將返回true并且把vval的值賦為&dow,意為“等待中”,_dispatch_client_callout的內(nèi)部會(huì)做一些判斷,但實(shí)際上調(diào)用了func函數(shù),到此block中的用戶代碼執(zhí)行結(jié)束。接下來就是
dispatch_atomic_maximally_synchronizing_barrier函數(shù),這是個(gè)宏函數(shù),這個(gè)函數(shù)編譯過后成為了cpuid指令,它的作用可以讓其他線程讀取到未初始化的對(duì)象欲執(zhí)行猜測(cè)能被判斷為“猜測(cè)未命中”,從而可以使這些線程進(jìn)入dispatch_once_f的另外一個(gè)分支(else分支)進(jìn)行等待。完成后,使用dispatch_atomic_xchg進(jìn)行賦值,使其為DISPATCH_ONCE_DONE,即“完成”。接著是對(duì)信號(hào)量鏈表的處理,分兩種情況:1,block執(zhí)行過程中,沒有其他線程進(jìn)入本函數(shù)來等待,則
vval指向值保持為&dow,即tmp被賦值為&dow,即下方while循環(huán)不會(huì)被執(zhí)行,此分支結(jié)束。2,在block執(zhí)行過程中,有其他線程進(jìn)入本函數(shù)來等待,那么會(huì)構(gòu)造一個(gè)信號(hào)量鏈表(vval指向值變?yōu)樾盘?hào)量鏈的頭部,鏈表的尾部為&dow),此時(shí)就會(huì)進(jìn)入while循環(huán),在此while循環(huán)中,遍歷鏈表,逐個(gè)signal每個(gè)信號(hào)量,然后結(jié)束循環(huán)。while (!tmp->dow_next)此循環(huán)是等待在&dow上的,因?yàn)?strong>線程等待分支2會(huì)中途將val賦值為&dow,然后為dow_next賦值,這期間dow_next值為NULL,所以需要等待,下面是線程等待分支2的描述:當(dāng)執(zhí)行block分支1還沒有完成,而且有新的線程進(jìn)入到本函數(shù),則進(jìn)入線程等待分支,首先調(diào)用
_dispatch_get_thread_semaphore函數(shù)創(chuàng)建一個(gè)信號(hào)量,此信號(hào)量被賦值給dow.dow_sema。然后進(jìn)入一個(gè)無限for循環(huán),假如發(fā)現(xiàn)vval的指向值已經(jīng)為DISPATCH_ONCE_DONE,即“完成”,則直接break,然后調(diào)用_dispatch_put_thread_semaphore函數(shù)銷毀信號(hào)量并退出函數(shù)。假如vval的值并非DISPATCH_ONCE_DONE,則進(jìn)行一個(gè)“原子比較并交換”操作(此操作可以避免兩個(gè)等待線程同時(shí)操作鏈表帶來的問題),假如此時(shí)vval指向值已不再是tmp(這種情況發(fā)生在多個(gè)線程同時(shí)進(jìn)入線程 等待分支2 ,并交錯(cuò)修改鏈表)則for循環(huán)重新開始,再嘗試重新獲取一次vval來進(jìn)行同樣的操作;若指向值還是tmp,則將vval的指向值賦值為&dow,此時(shí)val->dow_next值為NULL,可能會(huì)使得block執(zhí)行分支1進(jìn)行while等待(如前述),緊接著執(zhí)行dow.dow_next = tmp這句來增加鏈表節(jié)點(diǎn)(同時(shí)也使得block執(zhí)行分支1的while等待結(jié)束),然后等待在信號(hào)量上,當(dāng)block執(zhí)行分支1完成并遍歷鏈表來signal時(shí),喚醒、釋放信號(hào)量,然后一切就完成了。
通過看實(shí)現(xiàn)代碼,大致可以知道dispatch_once是這樣的過程:
- 線程A執(zhí)行block時(shí),其它線程都需要等待。
- 線程A執(zhí)行完block應(yīng)該立即標(biāo)記任務(wù)為完成狀態(tài),然后遍歷信號(hào)量鏈來喚醒所有等待線程。
- 線程A遍歷信號(hào)量鏈來signal時(shí),任何其他新進(jìn)入函數(shù)的線程都應(yīng)該直接返回而無需等待。
- 線程A遍歷信號(hào)量鏈來signal時(shí),若有其它等待線程B仍在更新或試圖更新信號(hào)量鏈表,應(yīng)該保證線程B能正確完成其任務(wù):a.直接返回 b.等待在信號(hào)量上并很快又被喚醒。
- 線程B構(gòu)造信號(hào)量時(shí),應(yīng)該考慮線程A隨時(shí)可能改變狀態(tài)(等待、完成、遍歷信號(hào)量鏈表)。
- 線程B構(gòu)造信號(hào)量時(shí),應(yīng)該考慮到另一個(gè)線程C也可能正在更新或試圖更新信號(hào)量鏈,應(yīng)該保證B、C都能正常完成其任務(wù):a.增加鏈節(jié)并等待在信號(hào)量上 b.發(fā)現(xiàn)線程A已經(jīng)標(biāo)記“完成”然后直接銷毀信號(hào)量并退出函數(shù)。
總結(jié):
-
dispatch_once不是只執(zhí)行一次那么簡單。內(nèi)部還是很復(fù)雜的。onceToken在第一次執(zhí)行block之前,它的值由NULL變?yōu)橹赶虻谝粋€(gè)調(diào)用者的指針(&dow)。 -
dispatch_once是可以接受多次請(qǐng)求的,內(nèi)部會(huì)構(gòu)造一個(gè)鏈表來維護(hù)之。如果在block完成之前,有其它的調(diào)用者進(jìn)來,則會(huì)把這些調(diào)用者放到一個(gè)waiter鏈表中(在else分支中的代碼)。 - waiter鏈表中的每個(gè)調(diào)用者會(huì)等待一個(gè)信號(hào)量(dow.dow_sema)。在block執(zhí)行完了后,除了將onceToken置為DISPATCH_ONCE_DONE外,還會(huì)去遍歷waiter鏈中的所有waiter,拋出相應(yīng)的信號(hào)量,以告知waiter們調(diào)用已經(jīng)結(jié)束了。
- 兩個(gè)類相互調(diào)用其單例方法時(shí),調(diào)用者TestA作為一個(gè)waiter,在等待TestB中的block完成,而TestB中block的完成依賴于TestA中單例函數(shù)的block的執(zhí)行完成,而TestA中的block想要完成還需要TestB中的block完成……兩個(gè)人都在相互等待對(duì)方的完成,這就成了一個(gè)死鎖。如果在
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),但如果是A->B->C->A->B->C->A這樣的復(fù)雜鏈狀循環(huán)的話,是很難直觀判斷出是否有循環(huán)的。如果隱含更加復(fù)雜的循環(huán)鏈,天曉得會(huì)出現(xiàn)在哪兒)。