必要性に追われて、メモリバリアを使ったコードを書くことになったので、自分用のメモとして残しておいたものです。
#メモリバリア
MemoryBarrier(メモリバリア)または MemoryFence(メモリフェンス)とは、その前後のメモリ操作の順序性を制限するCPUの命令の一種である。
C++の標準ライブラリにおける、メモリバリアの定義はこちら。
namespace std {
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
}
※この記事では、memory_order_acquireと、memory_order_releaseだけを扱っています。
#マルチスレッドプログラムにおける利用例
#include <iostream>
#include <atomic>
#include <thread>
int data;
std::atomic<bool> ready(false);
void f()
{
while (!ready.load(std::memory_order_acquire)) {
}
std::cout << data << std::endl;
}
int main()
{
std::thread t(f);
data = 765;
ready.store(true, std::memory_order_release);
t.join();
}
##アトミック変数
std::atomic
アトミック変数とは、不可分な読み出しや書き込み、および読み書きを同時に行える変数である。
アトミック変数への操作は、他のスレッドによって割り込まれることがない。
つまり、アトミック変数は、その値の変更操作がスレッドセーフであるといえる。
std::atomicには、専用の操作関数が用意されている。
読み込み用メンバ関数 | load |
書き込み用メンバ関数 | store |
###load-acquire
値の読み出し後の実行順序を制限する。
while( !ready.load( std::memory_order_acquire ) ) {
}
// ここより下のコードが、
// std::memory_order_acquire を指定した値の読み出しより、
// 後に実行されることが保証される
std::cout << data << std::endl;
###store-release
値の書き込み前の実行順序を制限する。
data = 765
// ここより上のコードが、
// std::memory_order_release を指定した値の書き込みより、
// 前に実行されることが保証される
ready.store( true, std::memory_order_release );
#mutexとの違い
MUTual EXclusion (相互排他、排他制御)
mutexとは、クリティカルセクションでアトミック性を確保するための排他制御のことである。
クリティカルセクションを同時に実行するスレッドが1つだけであることを保証する。
##ロックガード
std::lock_guard
C++の標準ライブラリで提供されている仕組み。
uint32_t g_x;
std::mutex mtx;
void add_value()
{
std::lock_guard<std::mutex> lock(mtx);
// クリティカルセクション ここから
g_x++;
// クリティカルセクション ここまで
}
void safe_print()
{
std::lock_guard<std::mutex> lock(mtx);
// クリティカルセクション ここから
printf( "%d", g_x );
// クリティカルセクション ここまで
}
共有変数の値を安全(スレッドセーフ)に読み込む、書き出すという点では同じ効果を得られるが、
以下の2つの特徴の違いを正しく理解しておく必要がある。
- メモリバリアは順序性を制御するものであり、クリティカルセクションを設けるものではない
- mutexのロックによるクリティカルセクションの実行中は、他のスレッドがその実行を妨げられる