讓我們先看看dispatch_once
的實現(Grand
Central Dispatch是開源的,大家可以到git://git.macosforge.org/libdispatch.git克隆源碼)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
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__ void dispatch_once( dispatch_once_t
*val, dispatch_block_t
block) { struct
Block_basic *bb = ( void
*)block; 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; struct
_dispatch_once_waiter_s dow = { NULL ,
0
}; struct
_dispatch_once_waiter_s *tail, *tmp; _dispatch_thread_semaphore_t
sema; if
(dispatch_atomic_cmpxchg(vval, NULL ,
&dow)) { dispatch_atomic_acquire_barrier(); //這是一個空的宏函數,什麼也不做 _dispatch_client_callout(ctxt,
func); dispatch_atomic_maximally_synchronizing_barrier(); //dispatch_atomic_release_barrier();
// assumed contained in above tmp
= dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE); tail
= &dow; while
(tail != tmp) { while
(!tmp->dow_next) { _dispatch_hardware_pause(); } sema
= tmp->dow_sema; tmp
= ( struct
_dispatch_once_waiter_s*)tmp->dow_next; _dispatch_thread_semaphore_signal(sema); } }
else
{ dow.dow_sema
= _dispatch_get_thread_semaphore(); for
(;;) { tmp
= *vval; if
(tmp == DISPATCH_ONCE_DONE) { break ; } dispatch_atomic_store_barrier(); 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); } } |
一堆宏函數加一堆讓人頭大的線程同步代碼。一步一步看:
dispatch_once
內部其實是調用了dispatch_once_f
,f指的是調用c函數(沒有f指的是調用block),實際上執行block最終也是調用c函數(詳見我的《Block非官方編程指南》)。當dispatch_once_f
被調用時,val
是外部傳入的predicate
,ctxt
傳入的是Block的指針,func
指的是Block內部的執行體函數,執行它就是執行block。
接下來是聲明瞭一堆變量,vval
是volatile標記過的val
,volatile修飾符的作用上一篇已經介紹過,告訴編譯器此指針指向的值隨時可能被其他線程改變,從而使得編譯器不對此指針進行代碼編譯優化。
dow意爲dispatch_once wait
dispatch_atomic_cmpxchg
是上一篇我們講過的“原子比較交換函數”__sync_bool_compare_and_swap
的宏替換,接下來進入分支:
1.執行block的分支
當dispatch_once
第一次執行時,predicate
也即val
爲0,那麼此“原子比較交換函數”將返回true並將vval
指向值賦值爲&dow
,即爲“等待中”,_dispatch_client_callout
其內部做了一些判定,但實際上是調用了func
而已。到此,block中的用戶代碼執行完畢。
接下來就是上篇提及的cpuid
指令等待,使得其他線程的【讀取到未初始化值的】預執行能被判定爲猜測未命中,從而使得這些線程能夠進入dispatch_once_f
裏的另一個分支從而進行等待。
cpuid
指令完畢後,調用dispatch_atomic_xchg
進行賦值,置其爲DISPATCH_ONCE_DONE,即“完成”,這裏dispatch_atomic_xchg
是內建“原子交換函數”__sync_swap
的優化版宏替換,其將第二個參數的值賦給第一個參數(解引用指針),然後返回第一個參數被賦值前的解引用值,其原型爲:
1
|
type
__sync_swap(type *ptr, type value, ...) |
接下來是對信號量鏈的處理:
-
在block執行過程中,沒有其他線程進入本函數來等待,則
vval
指向值保持爲&dow
,即tmp
被賦值爲&dow
,即下方while循環不會被執行,此分支結束。 -
在block執行過程中,有其他線程進入本函數來等待,那麼會構造一個信號量鏈表(
vval
指向值變爲信號量鏈的頭部,鏈表的尾部爲&dow
),此時就會進入while循環,在此while循環中,遍歷鏈表,逐個signal每個信號量,然後結束循環。
while (!tmp->dow_next)
此循環是等待在&dow上,因爲線程等待分支#2會中途將val
賦值爲&dow
,然後爲->dow_next
賦值,這期間->dow_next
值爲NULL,需要等待,詳見下面線程等待分支#2的描述
_dispatch_hardware_pause
此句是爲了提示cpu減少額外處理,提升性能,節省電力。
2.線程等待分支
當執行block分支#1未完成,且有線程再進入本函數時,將進入線程等待分支:
先調用_dispatch_get_thread_semaphore
創建一個信號量,此信號量被賦值給dow
.dow_sema
。
然後進入一個無限for循環,假如發現vval
的指向值已經爲DISPATCH_ONCE_DONE
,即“完成”,則直接break,然後調用_dispatch_put_thread_semaphore
函數銷燬信號量並退出函數。
_dispatch_get_thread_semaphore
內部使用的是“有即取用,無即創建”策略來獲取信號量。
_dispatch_put_thread_semaphore
內部使用的是“銷燬舊的,存儲新的”策略來緩存信號量。
假如vval
的解引用值並非DISPATCH_ONCE_DONE
,則進行一個“原子比較並交換”操作(此操作可以避免兩個等待線程同時操作鏈表帶來的問題),假如此時vval
指向值已不再是tmp
(這種情況發生在多個線程同時進入線程等待分支#2,並交錯修改鏈表)則for循環重新開始,再嘗試重新獲取一次vval來進行同樣的操作;若指向值還是tmp,則將vval
的指向值賦值爲&dow
,此時val->dow_next
值爲NULL,可能會使得block執行分支#1進行while等待(如前述),緊接着執行dow.dow_next
= tmp
這句來增加鏈表節點(同時也使得block執行分支#1的while等待結束),然後等待在信號量上,當block執行分支#1完成並遍歷鏈表來signal時,喚醒、釋放信號量,然後一切就完成了。
小結
綜上所述,dispatch_once的主要處理的情況如下:
- 線程A執行Block時,任何其它線程都需要等待。
- 線程A執行完Block應該立即標記任務完成狀態,然後遍歷信號量鏈來喚醒所有等待線程。
- 線程A遍歷信號量鏈來signal時,任何其他新進入函數的線程都應該直接返回而無需等待。
- 線程A遍歷信號量鏈來signal時,若有其它等待線程B仍在更新或試圖更新信號量鏈,應該保證此線程B能正確完成其任務:a.直接返回 b.等待在信號量上並很快又被喚醒。
- 線程B構造信號量時,應該考慮線程A隨時可能改變狀態(“等待”、“完成”、“遍歷信號量鏈”)。
- 線程B構造信號量時,應該考慮到另一個線程C也可能正在更新或試圖更新信號量鏈,應該保證B、C都能正常完成其任務:a.增加鏈節並等待在信號量上 b.發現線程A已經標記“完成”然後直接銷燬信號量並退出函數。
總結
無鎖的線程同步編程非常精巧,爲了提升效率,每一處線程競爭都必須被考慮到並妥善處理。但這種編程方式又極其令人神往,原子操作的魅力便在於此,它就像是一個精密的鐘表,每一處接合都如此巧妙