52個有效方法(45) - 使用dispatch_once來執(zhí)行只需運行一次的線程安全代碼

dispatch_once

void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
  • 其中第一個參數(shù)predicate,該參數(shù)是檢查后面第二個參數(shù)所代表的代碼塊是否被調(diào)用的謂詞。

  • 第二個參數(shù)則是在整個應(yīng)用程序中只會被調(diào)用一次的代碼塊。dispach_once函數(shù)中的代碼塊只會被執(zhí)行一次,而且還是線程安全的。

使用dispatch_once創(chuàng)建單例
+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static id instance = nil;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

使用dispatch_once可以簡化代碼并且徹底保證線程安全,開發(fā)者根本無須擔心加鎖或同步。所有問題都有GCD在底層處理。由于每次調(diào)用時都必須使用完全相同的標記,所以標記要聲明成static。把該變量定義在static作用域中,可以保證編譯器在每次執(zhí)行shareInstance方法時都會復(fù)用這個變量,而不會創(chuàng)建新變量。此外,dispatch_once更高效。

單例 dispatch_once的淺析

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);

//注意這個內(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

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í)行期進入的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.調(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之后,會調(diào)用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沒有結(jié)束,那么就將這些
            // 后續(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);
    }
}

  • dispatch_once不是只執(zhí)行一次那么簡單。內(nèi)部還是很復(fù)雜的。onceToken在第一次執(zhí)行block之前,它的值由NULL變?yōu)橹赶虻谝粋€調(diào)用者的指針(&dow)。

  • dispatch_once是可以接受多次請求的,內(nèi)部會構(gòu)造一個鏈表來維護之。如果在block完成之前,有其它的調(diào)用者進來,則會把這些調(diào)用者放到一個waiter鏈表中(在else分支中的代碼)。

  • waiter鏈表中的每個調(diào)用者會等待一個信號量(dow.dow_sema)。在block執(zhí)行完了后,除了將onceToken置為DISPATCH_ONCE_DONE外,還會去遍歷waiter鏈中的所有waiter,拋出相應(yīng)的信號量,以告知waiter們調(diào)用已經(jīng)結(jié)束了。

dispatch_once大致的過程
  • 線程A執(zhí)行block時,其它線程都需要等待。

  • 線程A執(zhí)行完block應(yīng)該立即標記任務(wù)為完成狀態(tài),然后遍歷信號量鏈來喚醒所有等待線程。

  • 線程A遍歷信號量鏈來signal時,任何其他新進入函數(shù)的線程都應(yīng)該直接返回而無需等待。

  • 線程A遍歷信號量鏈來signal時,若有其它等待線程B仍在更新或試圖更新信號量鏈表,應(yīng)該保證線程B能正確完成其任務(wù):a.直接返回 b.等待在信號量上并很快又被喚醒。

  • 線程B構(gòu)造信號量時,應(yīng)該考慮線程A隨時可能改變狀態(tài)(等待、完成、遍歷信號量鏈表)。

  • 線程B構(gòu)造信號量時,應(yīng)該考慮到另一個線程C也可能正在更新或試圖更新信號量鏈,應(yīng)該保證B、C都能正常完成其任務(wù):a.增加鏈節(jié)并等待在信號量上 b.發(fā)現(xiàn)線程A已經(jīng)標記“完成”然后直接銷毀信號量并退出函數(shù)。

要點
  1. 經(jīng)常需要編寫“只需要執(zhí)行一次的線程安全代碼”。通過GCD所提供的dispatch_once函數(shù),很容易就能實現(xiàn)此功能。

  2. 標記應(yīng)該聲明在static或global作用域中,這樣的話,在把只需執(zhí)行一次的塊傳給dispatch_once函數(shù)時,傳進去的標記也是相同的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容