在開發(fā)中使用單例是最經(jīng)常不過的事兒了,最常用的就是dispatch_once這個函數(shù),這個函數(shù)可以使其參數(shù)內的block塊只在全局執(zhí)行一次從而達到目的,不過這個函數(shù)要是用的稍微“巧”了的話,就會出問題。比如看下面這段代碼:
#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的對象,猜猜結果會怎樣?卡死了,直接不動了,造成死鎖了(我等了30秒還是沒動靜……)。這個情況曾經(jīng)在幫同學查問題的時候遇見過一次。現(xiàn)在暫停程序,查看調用棧的狀態(tài)如下圖所示:
發(fā)現(xiàn)這么幾個函數(shù)調用的很頻繁:dispatch_once_f
和shareInstanceA
和shareInstanceB
,而且在棧頂部的函數(shù)是semaphore_wait_trap
和dispatch_once_f
,程序最后是在dispatch_once_f
卡死的,在這兒出現(xiàn)的問題,那么這個函數(shù)有巨大的懷疑,然后就想辦法搞到這個函數(shù)的源碼,因為GCD部分是開源的,可以找到代碼,尋找途徑一:http://libdispatch.macosforge.org/trac/browser#tags/libdispatch-200/src ,在這個路徑下,有一個once.c的文件,里面就有此函數(shù)的代碼。路徑二:在git bash中輸入命令:git clone git://git.macosforge.org/libdispatch.git,在下載下來的文件中找到src文件夾下的once.c文件。現(xiàn)在就看看就看看它的內部實現(xiàn)吧,為了后面的內容所提及的代碼,也把once.h文件的代碼放到下面了,由于這個文件的代碼不多,放到第一部分:
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);
//注意這個內聯(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)在要提前補習一點知識。來看上面那些代碼的大概意思:
大體意思
看那個內聯(lián)函數(shù)的注釋,這其實是dispatch_once
讀取端的實現(xiàn),對應還有寫入端(后面有提到),DISPATCH_EXPECT
函數(shù)是告訴cpu *predicate的
值等于 ~0l 是很有可能的判定結果,DISPATCH_EXPECT展開就是 __builtin_expect((x), (v))
,它是GCC引進的宏,其作用就是幫助編譯器判斷條件跳轉的預期值,避免跳轉造成時間浪費。并沒有改變其對真值的判斷。主要目的就是增加效率,降低負載,為什么要降低負載呢,原因如下。
原因
先來看dispatch_once的三個使用時的所在場景:
- 第一次執(zhí)行,block需要調用,調用結束后要改變標記量
- 不是第一次執(zhí)行,并且這時候步驟1中的沒執(zhí)行完,當前線程需要等待步驟1的完成
- 不是第一次執(zhí)行,但是這時候步驟1執(zhí)行結束,當前線程跳過block執(zhí)行后續(xù)的任務
對于場景1,性能瓶頸在于block所執(zhí)行的任務,而不在dispatch_once
函數(shù)本身。場景2,發(fā)生的幾率不大,即時遇到這種情況,只需等待前面的線程執(zhí)行結束就可。場景3,則可能經(jīng)常遇到,前面的dispatch_once一旦執(zhí)行結束,后面所有的線程遇到了之后都是場景3的情況。
dispatch_once本意是只執(zhí)行一次就結束它的使命,具有一次性,一旦執(zhí)行結束后,它就沒意義了,希望后面的任務在執(zhí)行中基本不會受其影響,一旦有大的影響,就會造成性能負擔,因此希望它盡可能降低對后續(xù)調用的負載。那么多大影響才算“較小”的影響,這需要一個基準線,這個基準線就是非線程安全的純if判斷語句,而dispatch_once
確實接近這個基準值。
解決辦法
那么要采取什么辦法解決這個問題呢,在翻閱一大堆外文資料、博客、翻譯后,發(fā)現(xiàn)這里面涉及到一些關于CPU的知識:
cpu的分支預測和預執(zhí)行
流水線特性使得CPU能更快地執(zhí)行線性指令序列,但是當遇到條件判斷分支時,麻煩來了,在判定語句返回結果之前,cpu不知道該執(zhí)行哪個分支,那就得等著(術語叫做pipeline stall),所以,CPU會進行預執(zhí)行推理,cpu先猜測一個可能的分支,然后開始執(zhí) 行分支中的指令。現(xiàn)代CPU一般都能做到超過90%的猜測命中率。然后當判定語句返回,如果cpu猜錯分支,那么之前進行的執(zhí)行都會被拋棄,然后從正確的分支重新開始執(zhí)行(在once.c文件里的源代碼注釋里提到之)。
在dispatch_once中,唯一一個判斷分支就是predicate,dispatch_once會讓CPU預先執(zhí)行條件不成立的分支,這樣可以大大提升函數(shù)執(zhí)行速度(在once.c文件里的源代碼注釋里提到之)。但是這樣的預執(zhí)行導致的結果是使用了未初始化的obj并將函數(shù)返回,這顯然不是預期結果。
不對稱barrier
編寫barrier時,應該是對稱的,在寫入端,要有一個barrier來保證順序寫入,同時,在讀取端,也要有一個barrier來保證順序讀取。但是,我們的dispatch_once實現(xiàn)要求寫入端快不快無所謂,而讀取端盡可能的快。所以,我們要解決前述的預執(zhí)行引起的問題。
當一個預執(zhí)行最終被發(fā)現(xiàn)是錯誤的猜測時,所有的預執(zhí)行狀態(tài)以及結果都會被清除,然后cpu會從判斷分支處重新執(zhí)行正確的分支,也就意味著被讀取的未初始化的obj也會被拋棄,然后讀取。假如dispatch_once能做到在執(zhí)行完block并正確賦值給obj后,告訴其它cpu核心:剛才都猜錯了!然后cpu就會重新從分支處開始執(zhí)行,進而獲取正確的obj值并返回。
從最早的預執(zhí)行到條件判斷語句最終結果被計算出來,這之間有很長時間(記作Ta),具體多長取決于cpu的設計,但是不論如何,這個時間最多幾十圈cpu時鐘時間(在once.c文件里的源代碼注釋里提到之),假如寫入端能在【初始化并寫入obj】與【置predicate值】之間等待足夠長的時間Tb使得Tb大于等于Ta,那問題就都解決了。
如果覺得這個”解決”難以理解,那么反過來思考,假如Tb小于Ta,那么Tb就有可能被Ta完全包含,也就是說,另一個線程B(耗時為Ta)在預執(zhí)行讀取了未初始化的obj值之后,回過頭來確認猜測正確性時,predicate可能被執(zhí)行block的線程A置為了“完成”,這就導致線程B認為自己的預執(zhí)行有效(實際上它讀取了未初始化的值)。而假如Tb大于等于Ta,任何讀取了未初始化的obj值的預執(zhí)行都會被判定為未命中,從而進入內層dispatch_once而進行等待。
要保證足夠的等待時間,需要一些trick。在intel的CPU上,dispatch_once動用了cpuid指令來達成這個目的。cpuid本來是用作取得cpu的信息,但是這個指令也同時強制將指令流串行化,并且這個指令是需要比較長的執(zhí)行時間的(在某些cpu上,甚至需要幾百圈cpu時鐘),這個時間Tb足夠超過Ta了。
接著大體意思繼續(xù)看
DISPATCH_EXPECT
函數(shù)的作用是使得CPU在猜測上有更大的幾率提高正確率,猜測到正確的分支,最重要的是,這一句是個簡單的if判定語句,負載無限接近基準值。到此,讀取端的介紹完畢。
這是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.應用程序調用的入口
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
struct Block_basic *bb = (void *)block;
// 2. 內部邏輯
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í)行期進入的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.調用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之后,會調用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. 如果這個時候tmp的next指針還沒更新完畢,就等待一會,提示cpu減少額外處理,提升性能,節(jié)省電力。
while (!tmp->dow_next)
{
_dispatch_hardware_pause();
}
// 11. 取出當前的信號量,告訴等待者,這次更改請求完成了,輪到下一個了
sema = tmp->dow_sema;
tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
_dispatch_thread_semaphore_signal(sema);
}
} else
{
// 12. 非首次請求,進入此邏輯塊
dow.dow_sema = _dispatch_get_thread_semaphore();
// 13. 遍歷每一個后續(xù)請求,如果狀態(tài)已經(jīng)是Done,直接進行下一個
// 同時該狀態(tài)檢測還用于避免在后續(xù)wait之前,信號量已經(jīng)發(fā)出(signal)造成
// 的死鎖
for (;;)
{
tmp = *vval;
if (tmp == DISPATCH_ONCE_DONE)
{
break;
}
dispatch_atomic_store_barrier();
// 14. 如果當前dispatch_once執(zhí)行的block沒有結束,那么就將這些
// 后續(xù)請求添加到鏈表當中
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ù)和一堆線程同步代碼,看的頭大……結合前面提到的和源代碼注釋一點一點的看:
dispatch_once
函數(shù)的里面其實是調用了dispatch_once_f
函數(shù),而f的意思是C函數(shù)(沒有帶f的是調用了block),但是block最終還是調用了C函數(shù)。當調用了dispatch_once_f
函數(shù)的時候,val
是外部傳入的predicate
,ctxt
指的是外部傳入的block的指針,func
是block里的具體執(zhí)行體函數(shù),執(zhí)行func
就是執(zhí)行block。接著聲明了一堆變量,
vval
是volatile標記了的val
,volatile修飾符的意思大概是告訴編譯器:這個指針所指向的值,可能隨時會被其他線程所改變,使編譯器不再對此指針進行代碼編譯優(yōu)化。dow意為dispatch_once_wait
。dispatch_atomic_cmpxchg
是“原子比較交換函數(shù)”__sync_bool_compare_and_swap
的宏替換,然后進入分支1:執(zhí)行block分支,當dispatch_once
第一次執(zhí)行時,predicate
也就是val
的值為0,這時候“原子比較交換函數(shù)”將返回true并且把vval
的值賦為&dow
,意為“等待中”,_dispatch_client_callout
的內部會做一些判斷,但實際上調用了func
函數(shù),到此block中的用戶代碼執(zhí)行結束。接下來就是
dispatch_atomic_maximally_synchronizing_barrier
函數(shù),這是個宏函數(shù),這個函數(shù)編譯過后成為了cpuid指令,它的作用可以讓其他線程讀取到未初始化的對象欲執(zhí)行猜測能被判斷為“猜測未命中”,從而可以使這些線程進入dispatch_once_f
的另外一個分支(else分支)進行等待。完成后,使用dispatch_atomic_xchg
進行賦值,使其為DISPATCH_ONCE_DONE
,即“完成”。接著是對信號量鏈表的處理,分兩種情況:1,block執(zhí)行過程中,沒有其他線程進入本函數(shù)來等待,則
vval
指向值保持為&dow
,即tmp
被賦值為&dow
,即下方while循環(huán)不會被執(zhí)行,此分支結束。2,在block執(zhí)行過程中,有其他線程進入本函數(shù)來等待,那么會構造一個信號量鏈表(vval
指向值變?yōu)樾盘柫挎湹念^部,鏈表的尾部為&dow
),此時就會進入while循環(huán),在此while循環(huán)中,遍歷鏈表,逐個signal
每個信號量,然后結束循環(huán)。while (!tmp->dow_next)
此循環(huán)是等待在&dow
上的,因為線程等待分支2會中途將val賦值為&dow,然后為dow_next賦值,這期間dow_next值為NULL,所以需要等待,下面是線程等待分支2的描述:當執(zhí)行block分支1還沒有完成,而且有新的線程進入到本函數(shù),則進入線程等待分支,首先調用
_dispatch_get_thread_semaphore
函數(shù)創(chuàng)建一個信號量,此信號量被賦值給dow.dow_sema
。然后進入一個無限for循環(huán),假如發(fā)現(xiàn)vval
的指向值已經(jīng)為DISPATCH_ONCE_DONE
,即“完成”,則直接break,然后調用_dispatch_put_thread_semaphore
函數(shù)銷毀信號量并退出函數(shù)。假如vval的值并非DISPATCH_ONCE_DONE
,則進行一個“原子比較并交換”操作(此操作可以避免兩個等待線程同時操作鏈表帶來的問題),假如此時vval
指向值已不再是tmp
(這種情況發(fā)生在多個線程同時進入線程 等待分支2 ,并交錯修改鏈表)則for循環(huán)重新開始,再嘗試重新獲取一次vval
來進行同樣的操作;若指向值還是tmp
,則將vval
的指向值賦值為&dow
,此時val->dow_next值為NULL
,可能會使得block執(zhí)行分支1進行while等待(如前述),緊接著執(zhí)行dow.dow_next = tmp
這句來增加鏈表節(jié)點(同時也使得block執(zhí)行分支1的while等待結束),然后等待在信號量上,當block執(zhí)行分支1完成并遍歷鏈表來signal時,喚醒、釋放信號量,然后一切就完成了。
通過看實現(xiàn)代碼,大致可以知道dispatch_once是這樣的過程:
- 線程A執(zhí)行block時,其它線程都需要等待。
- 線程A執(zhí)行完block應該立即標記任務為完成狀態(tài),然后遍歷信號量鏈來喚醒所有等待線程。
- 線程A遍歷信號量鏈來signal時,任何其他新進入函數(shù)的線程都應該直接返回而無需等待。
- 線程A遍歷信號量鏈來signal時,若有其它等待線程B仍在更新或試圖更新信號量鏈表,應該保證線程B能正確完成其任務:a.直接返回 b.等待在信號量上并很快又被喚醒。
- 線程B構造信號量時,應該考慮線程A隨時可能改變狀態(tài)(等待、完成、遍歷信號量鏈表)。
- 線程B構造信號量時,應該考慮到另一個線程C也可能正在更新或試圖更新信號量鏈,應該保證B、C都能正常完成其任務:a.增加鏈節(jié)并等待在信號量上 b.發(fā)現(xiàn)線程A已經(jīng)標記“完成”然后直接銷毀信號量并退出函數(shù)。
總結:
-
dispatch_once
不是只執(zhí)行一次那么簡單。內部還是很復雜的。onceToken在第一次執(zhí)行block之前,它的值由NULL變?yōu)橹赶虻谝粋€調用者的指針(&dow)。 -
dispatch_once
是可以接受多次請求的,內部會構造一個鏈表來維護之。如果在block完成之前,有其它的調用者進來,則會把這些調用者放到一個waiter鏈表中(在else分支中的代碼)。 - waiter鏈表中的每個調用者會等待一個信號量(dow.dow_sema)。在block執(zhí)行完了后,除了將onceToken置為DISPATCH_ONCE_DONE外,還會去遍歷waiter鏈中的所有waiter,拋出相應的信號量,以告知waiter們調用已經(jīng)結束了。
- 兩個類相互調用其單例方法時,調用者TestA作為一個waiter,在等待TestB中的block完成,而TestB中block的完成依賴于TestA中單例函數(shù)的block的執(zhí)行完成,而TestA中的block想要完成還需要TestB中的block完成……兩個人都在相互等待對方的完成,這就成了一個死鎖。如果在
dispatch_once
函數(shù)的block塊執(zhí)行期間,循環(huán)進入自己的dispatch_once
函數(shù),會造成鏈表一直增長,同樣也會造成死鎖。(這里只是簡單的A->B->A->B->A這樣的循環(huán),也可以是A->A->A這樣的更加直接的循環(huán),但如果是A->B->C->A->B->C->A這樣的復雜鏈狀循環(huán)的話,是很難直觀判斷出是否有循環(huán)的。如果隱含更加復雜的循環(huán)鏈,天曉得會出現(xiàn)在哪兒)。