アトミック操作は、スレッド間の対話型データの最もきめ細かい同期操作であり、スレッド間で特定の値を読み書きするアトミック性を保証できます。
同期のために重いミューテックスロックを追加する必要がないため、非常に軽量であり、コア間でスケジューリングを切り替える必要がなく、効率が非常に高くなります。。
したがって、アトミック操作の使用方法には、サポートを提供するために各プラットフォームの下に関連するAPIがあり、gccやclangなどのコンパイラーをサポートするためにコンパイラーレベルの__builtinインターフェースも提供されます。
- WindowsInterlockedxxxおよびInterlockedxxx64シリーズAPI
- macosxのOSAtomicXXXシリーズAPI
- gcc
__sync_val_compare_and_swap
および__sync_val_compare_and_swap_8
その他の__builtinインターフェース - x86およびx86_64アーキテクチャの
lock
アセンブリ手順 - tboxのクロスプラットフォームアトミックインターフェイス
tboxインターフェースの使用
tb_atomic_fetch_and_add
例としてtboxのインターフェースを取り上げます。名前が示すように、このAPIは最初に元の値を読み取り、次に値を追加します。
// 相当于原子进行:b = *a++;
tb_atomic_t a = 0;
tb_long_t b = tb_atomic_fetch_and_add(&a, 1);
最初に加算計算を実行してから結果を返す必要がある場合は、次を使用できます。
// 相当于原子进行: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のラップのレイヤーにすぎません。
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、などの論理計算も実行できます。使用法は似ているため、ここでは詳しく説明しません。
以下の簡単な例を見て、実際に使用してみましょう。アトムには、次のような多くのアプリケーションシナリオがあります。
- スピンロックの実装に使用
- ロックフリーキューを実装するために使用されます
- スレッド間の状態同期
- シングルトンの実装に使用
などなど。。
スピンロックの実装
まず、単純なスピンロックを実装する方法を見てみましょう。標準のデモコードを統合するために、次のコードでは、例として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のマルチスレッド実装のほとんどは非常に細かい粒度であるため、tboxでは基本的にこのスピンロックがデフォルトで使用されます。
ほとんどの場合、スピンロックを使用しても問題はなく、切り替えて待機するためにカーネルモードに入る必要はありません。。
使用法は次のとおりです。
// 获取lock
tb_spinlock_enter(&lock);
// 一些同步操作
// ..
// 释放lock
tb_spinlock_leave(&lock);
上記のコードでは、init操作とexit操作が省略されていますが、実際の使用では、初期化と解放に応じて対応する処理を行うだけで十分です。。
クラスのpthread_once
実装
pthread_once
渡された関数が1回だけ呼び出されるようにするために、マルチスレッド関数に含めることができます。通常、グローバルシングルトンまたはTLSキーの初期化に使用できます。
例として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/