dispatch_once淺談

讓我們先看看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是外部傳入的predicatectxt傳入的是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, ...)

接下來是對信號量鏈的處理:

  1. 在block執行過程中,沒有其他線程進入本函數來等待,則vval指向值保持爲&dow,即tmp被賦值爲&dow,即下方while循環不會被執行,此分支結束。
  2. 在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的主要處理的情況如下:

  1. 線程A執行Block時,任何其它線程都需要等待。
  2. 線程A執行完Block應該立即標記任務完成狀態,然後遍歷信號量鏈來喚醒所有等待線程。
  3. 線程A遍歷信號量鏈來signal時,任何其他新進入函數的線程都應該直接返回而無需等待。
  4. 線程A遍歷信號量鏈來signal時,若有其它等待線程B仍在更新或試圖更新信號量鏈,應該保證此線程B能正確完成其任務:a.直接返回 b.等待在信號量上並很快又被喚醒。
  5. 線程B構造信號量時,應該考慮線程A隨時可能改變狀態(“等待”、“完成”、“遍歷信號量鏈”)。
  6. 線程B構造信號量時,應該考慮到另一個線程C也可能正在更新或試圖更新信號量鏈,應該保證B、C都能正常完成其任務:a.增加鏈節並等待在信號量上 b.發現線程A已經標記“完成”然後直接銷燬信號量並退出函數。

總結

無鎖的線程同步編程非常精巧,爲了提升效率,每一處線程競爭都必須被考慮到並妥善處理。但這種編程方式又極其令人神往,原子操作的魅力便在於此,它就像是一個精密的鐘表,每一處接合都如此巧妙

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章