聊聊原子操作那些事

原子操作,線程間交互數(shù)據(jù)最細(xì)粒度的同步操作,它可以保證線程間讀寫某個(gè)數(shù)值的原子性。

由于不需要加重量級(jí)的互斥鎖進(jìn)行同步,因此非常輕量,而且也不需要在內(nèi)核間來(lái)回切換調(diào)度,效率是非常高的。。

那如何使用原子操作了,各個(gè)平臺(tái)下都有相關(guān)api提供了支持,并且向gcc、clang這些編譯器,也提供了編譯器級(jí)的__builtin接口進(jìn)行支持

  1. windows的Interlockedxxx和Interlockedxxx64系列api
  2. macosx的OSAtomicXXX系列api
  3. gcc的__sync_val_compare_and_swap__sync_val_compare_and_swap_8等__builtin接口
  4. x86和x86_64架構(gòu)的lock匯編指令
  5. tbox的跨平臺(tái)原子接口

tbox接口使用

先拿tbox的tb_atomic_fetch_and_add接口為例,顧名思義,這個(gè)api會(huì)先讀取原有數(shù)值,然后在其基礎(chǔ)上加上一個(gè)數(shù)值:

// 相當(dāng)于原子進(jìn)行:b = *a++;
tb_atomic_t a = 0;
tb_long_t   b = tb_atomic_fetch_and_add(&a, 1);

如果需要先進(jìn)行add計(jì)算,再返回結(jié)果可以用:

// 相當(dāng)于原子進(jìn)行:b = ++*a;
tb_atomic_t a = 0;
tb_long_t   b = tb_atomic_add_and_fetch(&a, 1);

或者可以更加簡(jiǎn)化為:

tb_long_t b = tb_atomic_fetch_and_inc(&a);
tb_long_t b = tb_atomic_inc_and_fetch(&a);

那tbox在內(nèi)部如何去適配各個(gè)平臺(tái)的呢,我們可以簡(jiǎn)單看下,基本上就是對(duì)原生api進(jìn)行了一層wrap而已。

windows接口封裝

static __tb_inline__ tb_long_t tb_atomic_fetch_and_add_windows(tb_atomic_t* a, tb_long_t v)
{
    return (tb_long_t)InterlockedExchangeAdd((LONG __tb_volatile__*)a, v);
}
static __tb_inline__ tb_long_t tb_atomic_inc_and_fetch_windows(tb_atomic_t* a)
{
    return (tb_long_t)InterlockedIncrement((LONG __tb_volatile__*)a);
}

gcc接口的封裝

static __tb_inline__ tb_long_t tb_atomic_fetch_and_add_sync(tb_atomic_t* a, tb_long_t v)
{
    return __sync_fetch_and_add(a, v);
}

x86和x86_64架構(gòu)匯編實(shí)現(xiàn)

static __tb_inline__ tb_long_t tb_atomic_fetch_and_add_x86(tb_atomic_t* a, tb_long_t v)
{
    /*
     * xaddl v, [a]:
     *
     * o = [a]
     * [a] += v;
     * v = o;
     *
     * cf, ef, of, sf, zf, pf... maybe changed
     */
    __tb_asm__ __tb_volatile__ 
    (
#if TB_CPU_BITSIZE == 64
        "lock xaddq %0, %1 \n"          //!< xaddq v, [a]
#else
        "lock xaddl %0, %1 \n"          //!< xaddl v, [a]
#endif

        : "+r" (v) 
        : "m" (*a) 
        : "cc", "memory"
    );

    return v;
}

原子操作除了可以進(jìn)行對(duì)int32和int64數(shù)值加減乘除外,還可以進(jìn)行xor, or, and等邏輯計(jì)算,用法類似,這里就不多說(shuō)了。

下面我們?cè)賮?lái)個(gè)簡(jiǎn)單的實(shí)例,來(lái)實(shí)際運(yùn)用下,原子的應(yīng)用場(chǎng)景還是蠻多的,比如:

  • 用于實(shí)現(xiàn)自旋鎖
  • 用于實(shí)現(xiàn)無(wú)鎖隊(duì)列
  • 線程間的狀態(tài)同步
  • 用于實(shí)現(xiàn)單例

等等。。

自旋鎖的實(shí)現(xiàn)

我們先來(lái)看下如何去實(shí)現(xiàn)一個(gè)簡(jiǎn)單的自旋鎖,為了統(tǒng)一規(guī)范演示代碼,下面的代碼都用tbox提供的原子接口為例:

static __tb_inline_force__ tb_bool_t tb_spinlock_init(tb_spinlock_ref_t lock)
{
    // init 
    *lock = 0;

    // ok
    return tb_true;
}
static __tb_inline_force__ tb_void_t tb_spinlock_exit(tb_spinlock_ref_t lock)
{
    // exit 
    *lock = 0;
}
static __tb_inline_force__ tb_void_t tb_spinlock_enter(tb_spinlock_ref_t lock)
{
    /* 嘗試讀取lock的狀態(tài)值,如果還沒獲取到lock(狀態(tài)0),則獲取它(設(shè)置為1)
     * 如果對(duì)方線程已經(jīng)獲取到lock(狀態(tài)1),那么循環(huán)等待嘗試重新獲取
     *
     * 注:整個(gè)狀態(tài)讀取和設(shè)置,是原子的,無(wú)法被打斷
     */
    tb_size_t tryn = 5;
    while (tb_atomic_fetch_and_pset((tb_atomic_t*)lock, 0, 1))
    {
        // 沒獲取到lock,嘗試5次后,還不成功,則讓出cpu切到其他線程運(yùn)行,之后重新嘗試獲取
        if (!tryn--)
        {
            // yield
            tb_sched_yield();

            // reset tryn
            tryn = 5;
        }
    }
}
static __tb_inline_force__ tb_void_t tb_spinlock_leave(tb_spinlock_ref_t lock)
{
    // 釋放lock,此處無(wú)需原子,設(shè)置到一半被打斷,數(shù)值部位0,對(duì)方線程還是在等待中,不收影響
    *((tb_atomic_t*)lock) = 0;
}

這個(gè)實(shí)現(xiàn)非常簡(jiǎn)單,但是tbox里面,基本上默認(rèn)都是在使用這個(gè)spinlock,因?yàn)閠box里面大部分多線程實(shí)現(xiàn),粒度都被拆的很細(xì)

大部分情況下,用自旋鎖就ok了,無(wú)需進(jìn)入內(nèi)核態(tài)切換等待。。

使用方式如下:

// 獲取lock
tb_spinlock_enter(&lock);

// 一些同步操作
// ..

// 釋放lock
tb_spinlock_leave(&lock);

上面的代碼中,省略了init和exit操作,實(shí)際使用時(shí),在響應(yīng)初始化和釋放的地方,做相應(yīng)處理下就行了。。

pthread_once的實(shí)現(xiàn)

pthread_once 可以在多線程函數(shù)內(nèi),可以保證傳入的函數(shù)只被調(diào)用到一次,一般可以用來(lái)初始化全局單例或者TLS的key初始化

以tbox的接口為例,我先來(lái)來(lái)看下,這個(gè)函數(shù)的使用方式:


// 初始化函數(shù),只會(huì)被調(diào)用到一次
static tb_void_t tb_once_func(tb_cpointer_t priv)
{
    // 初始化一些單例對(duì)象,全局變量
    // 或者執(zhí)行一些初始化調(diào)用
}

// 線程函數(shù)
static tb_int_t tb_thread_func(tb_cpointer_t priv)
{
    // 全局存儲(chǔ)lock,并初始化為0
    static tb_atomic_t lock = 0;
    if (tb_thread_once(&lock, tb_once_func, "user data"))
    {
        // ok
    }
}

我們這里拿原子操作,可以簡(jiǎn)單模擬實(shí)現(xiàn)下這個(gè)函數(shù):

tb_bool_t tb_thread_once(tb_atomic_t* lock, tb_bool_t (*func)(tb_cpointer_t), tb_cpointer_t priv)
{
    // check
    tb_check_return_val(lock && func, tb_false);

    /* 原子獲取lock的狀態(tài)
     *
     * 0: func還沒有被調(diào)用
     * 1: 已經(jīng)獲取到lock,func正在被其他線程調(diào)用中
     * 2: func已經(jīng)被調(diào)用完成,并且func返回ok
     * -2: func已經(jīng)被調(diào)用,并且func返回失敗failed
     */
    tb_atomic_t called = tb_atomic_fetch_and_pset(lock, 0, 1);

    // func已經(jīng)被其他線程調(diào)用過(guò)了?直接返回
    if (called && called != 1) 
    {
        return called == 2;
    }
    // func還沒有被調(diào)用過(guò)?那么調(diào)用它
    else if (!called)
    {
        // 調(diào)用函數(shù)
        tb_bool_t ok = func(priv);

        // 設(shè)置返回狀態(tài)
        tb_atomic_set(lock, ok? 2 : -1);

        // ok?
        return ok;
    }
    // 正在被其他線程獲取到lock,func正在被調(diào)用中,還沒完成?嘗試等待lock
    else
    {
        // 此處簡(jiǎn)單的做了些sleep循環(huán)等待,直到對(duì)方線程func執(zhí)行完成
        tb_size_t tryn = 50;
        while ((1 == tb_atomic_get(lock)) && tryn--)
        {
            // wait some time
            tb_msleep(100);
        }
    }

    /* 重新獲取lock的狀態(tài),判斷是否成功
     * 
     * 成功:2
     * 超時(shí):1
     * 失敗:-2
     *
     * 此處只要不是2,都算失敗
     */
    return tb_atomic_get(lock) == 2;
}

64位原子操作

64位操作跟32位的接口使用方式,是完全一樣的,僅僅只是變量類型的區(qū)別:

  1. tbox中類型為tb_atomic64_t,接口改為tb_atomic64_xxxx
  2. gcc中類型為volatile long long,接口改為__sync_xxxx_8系列
  3. windows上則為Interlockedxxx64

具體使用方式參考32位,這里就不詳細(xì)介紹了。。


個(gè)人主頁(yè):TBOOX開源工程
原文出處:http://tboox.org/cn/2016/09/30/atomic-operation/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容