原子操作,線程間交互數據最細粒度的同步操作,它可以保證線程間讀寫某個數值的原子性。
由於不需要加重量級的互斥鎖進行同步,因此非常輕量,而且也不需要在內核間來回切換調度,效率是非常高的。。
那如何使用原子操作了,各個平臺下都有相關api提供了支持,並且向gcc、clang這些編譯器,也提供了編譯器級的__builtin接口進行支持
- windows的Interlockedxxx和Interlockedxxx64系列api
- macosx的OSAtomicXXX系列api
- gcc的
__sync_val_compare_and_swap
和__sync_val_compare_and_swap_8
等__builtin接口 - x86和x86_64架構的
lock
彙編指令 - tbox的跨平臺原子接口
tbox接口使用
先拿tbox的tb_atomic_fetch_and_add
接口爲例,顧名思義,這個api會先讀取原有數值,然後在其基礎上加上一個數值:
// 相當於原子進行:b = *a++;
tb_atomic_t a = 0;
tb_long_t b = tb_atomic_fetch_and_add(&a, 1);
如果需要先進行add計算,再返回結果可以用:
// 相當於原子進行:b = ++*a;
tb_atomic_t a = 0;
tb_long_t b = tb_atomic_add_and_fetch(&a, 1);
或者可以更加簡化爲:
tb_long_t b = tb_atomic_fetch_and_inc(&a);
tb_long_t b = tb_atomic_inc_and_fetch(&a);
那tbox在內部如何去適配各個平臺的呢,我們可以簡單看下,基本上就是對原生api進行了一層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架構彙編實現
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;
}
原子操作除了可以進行對int32和int64數值加減乘除外,還可以進行xor, or, and等邏輯計算,用法類似,這裏就不多說了。
下面我們再來個簡單的實例,來實際運用下,原子的應用場景還是蠻多的,比如:
- 用於實現自旋鎖
- 用於實現無鎖隊列
- 線程間的狀態同步
- 用於實現單例
等等。。
自旋鎖的實現
我們先來看下如何去實現一個簡單的自旋鎖,爲了統一規範演示代碼,下面的代碼都用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的狀態值,如果還沒獲取到lock(狀態0),則獲取它(設置爲1)
* 如果對方線程已經獲取到lock(狀態1),那麼循環等待嘗試重新獲取
*
* 注:整個狀態讀取和設置,是原子的,無法被打斷
*/
tb_size_t tryn = 5;
while (tb_atomic_fetch_and_pset((tb_atomic_t*)lock, 0, 1))
{
// 沒獲取到lock,嘗試5次後,還不成功,則讓出cpu切到其他線程運行,之後重新嘗試獲取
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,此處無需原子,設置到一半被打斷,數值部位0,對方線程還是在等待中,不收影響
*((tb_atomic_t*)lock) = 0;
}
這個實現非常簡單,但是tbox裏面,基本上默認都是在使用這個spinlock,因爲tbox裏面大部分多線程實現,粒度都被拆的很細
大部分情況下,用自旋鎖就ok了,無需進入內核態切換等待。。
使用方式如下:
// 獲取lock
tb_spinlock_enter(&lock);
// 一些同步操作
// ..
// 釋放lock
tb_spinlock_leave(&lock);
上面的代碼中,省略了init和exit操作,實際使用時,在響應初始化和釋放的地方,做相應處理下就行了。。
類pthread_once
的實現
pthread_once
可以在多線程函數內,可以保證傳入的函數只被調用到一次,一般可以用來初始化全局單例或者TLS的key初始化
以tbox的接口爲例,我先來來看下,這個函數的使用方式:
// 初始化函數,只會被調用到一次
static tb_void_t tb_once_func(tb_cpointer_t priv)
{
// 初始化一些單例對象,全局變量
// 或者執行一些初始化調用
}
// 線程函數
static tb_int_t tb_thread_func(tb_cpointer_t priv)
{
// 全局存儲lock,並初始化爲0
static tb_atomic_t lock = 0;
if (tb_thread_once(&lock, tb_once_func, "user data"))
{
// ok
}
}
我們這裏拿原子操作,可以簡單模擬實現下這個函數:
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的狀態
*
* 0: func還沒有被調用
* 1: 已經獲取到lock,func正在被其他線程調用中
* 2: func已經被調用完成,並且func返回ok
* -2: func已經被調用,並且func返回失敗failed
*/
tb_atomic_t called = tb_atomic_fetch_and_pset(lock, 0, 1);
// func已經被其他線程調用過了?直接返回
if (called && called != 1)
{
return called == 2;
}
// func還沒有被調用過?那麼調用它
else if (!called)
{
// 調用函數
tb_bool_t ok = func(priv);
// 設置返回狀態
tb_atomic_set(lock, ok? 2 : -1);
// ok?
return ok;
}
// 正在被其他線程獲取到lock,func正在被調用中,還沒完成?嘗試等待lock
else
{
// 此處簡單的做了些sleep循環等待,直到對方線程func執行完成
tb_size_t tryn = 50;
while ((1 == tb_atomic_get(lock)) && tryn--)
{
// wait some time
tb_msleep(100);
}
}
/* 重新獲取lock的狀態,判斷是否成功
*
* 成功:2
* 超時:1
* 失敗:-2
*
* 此處只要不是2,都算失敗
*/
return tb_atomic_get(lock) == 2;
}
64位原子操作
64位操作跟32位的接口使用方式,是完全一樣的,僅僅只是變量類型的區別:
- tbox中類型爲
tb_atomic64_t
,接口改爲tb_atomic64_xxxx
- gcc中類型爲
volatile long long
,接口改爲__sync_xxxx_8
系列 - windows上則爲Interlockedxxx64
具體使用方式參考32位,這裏就不詳細介紹了。。
個人主頁:TBOOX開源工程
原文出處:http://tboox.org/cn/2016/09/30/atomic-operation/