namespace std {
// C++11
enum memory_order {
memory_order_relaxed, memory_order_consume, memory_order_acquire,
memory_order_release, memory_order_acq_rel, memory_order_seq_cst
};
// C++20
enum class memory_order {
relaxed, consume, acquire, release, acq_rel, seq_cst
};
inline constexpr memory_order memory_order_relaxed = memory_order::relaxed;
inline constexpr memory_order memory_order_consume = memory_order::consume;
inline constexpr memory_order memory_order_acquire = memory_order::acquire;
inline constexpr memory_order memory_order_release = memory_order::release;
inline constexpr memory_order memory_order_acq_rel = memory_order::acq_rel;
inline constexpr memory_order memory_order_seq_cst = memory_order::seq_cst;
}
概要
コンパイラに許可されている最適化の一つに、「プログラムの意味を変えない限りにおいて、メモリアクセスの順番を変えたり、省略したりしてもよい」というものがある。また、マルチコアCPUにおいては、あるCPUコアによるメモリアクセスの順序が他のコアからも同じように見えるとは限らない。このような挙動はマルチスレッドプログラミングにおいて問題になることがある。 この問題への対処として、C++11では各スレッドの実行に順序付けをするための"happens before"(先行発生)という関係を定義し、それによってあるスレッドでの操作が他スレッドから可視になるか否かを定めている。 atomic変数においては、"release"操作によって書き込まれた値を"acquire"操作によって別のスレッドが読み出した場合に、そのrelease操作とacquire操作の間に順序付けが行われる。以下に例を挙げる。
#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; // (2)
}
int main()
{
std::thread t(f);
data = 3; // (1)
ready.store(true, std::memory_order_release);
t.join();
}
xxxxxxxxxx
#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; // (2)
}
int main()
{
std::thread t(f);
data = 3; // (1)
ready.store(true, std::memory_order_release);
t.join();
}
出力
3
atomic<bool>
型の変数ready
への読み書きに注目すると、main()
では変数ready
に true
を"release"操作として書き込み、f()
では"acquire"操作としての読み込みを true
が返されるまで繰り返している。よって、f()
のwhile
ループを抜けた時点で、main()
のready.store()
とf()
のready.load()
の間に順序付け(happens before関係)が成立している。
ここでさらに変数data
への読み書き(1), (2)に注目すると、(1)はready.store()
より前、(2)はready.load()
より後にあるので、以下のようなスレッド間の順序付け(happens before関係)が成立することになる。
(1) → ready.store()
→ ready.load()
→ (2)
よって、(1)における書き込みが(2)の時点で可視であることが保証される。
このようにしてC++のマルチスレッドプログラムにおける実行順序および可視性を理解することができる。
以下の列挙値はatomic変数への操作に指定可能な順序付けの種類を表す。
列挙値 | 説明 |
---|---|
relaxed |
スレッド間の順序付けの効果は一切持たない。 |
consume |
acquire操作と似ているが、それより弱い順序付けでの読み込みを行うことを指示する。acquire操作は後続の全ての操作に対して順序付けを行うのに対し、consume操作は読み込まれた値に依存(ただし条件分岐による依存は除く)する操作のみに順序付けを保証する点が異なる。store()など、書き込みのみを行う操作に対しては指定できない。(仕様検討中のため、一時的に非推奨) |
acquire |
acquire操作としての読み込みを行うことを指示する。store()など、書き込みのみを行う操作に対しては指定できない。 |
release |
release操作としての書き込みを行うことを指示する。load()など、読み込みのみを行う操作に対しては指定できない。 |
acq_rel |
読み込みと書き込みを同時に行う操作(Read-Modify-Write操作)に対してのみ指定することができ、acquireとreleaseを合わせた効果を持つ。 |
seq_cst |
acquire(読み込み操作の場合)、release(書き込み操作の場合)、acq_rel(Read-Modify-Write操作の場合)としての効果を持つ。さらに、同じseq_cstが指定されている他のatomic操作との間での順序一貫性も保証する。これは最も強い保証であり、標準のatomic操作におけるデフォルトのメモリオーダーとして使用される。「seq_cst」は「sequential consistency(順序一貫性)」を意味する。 |
バージョン
言語
- C++11
処理系
- Clang: ??
- GCC: 4.7.0 ✅
- ICC: ??
- Visual C++: 2012 ✅, 2013 ✅
参照
- そろそろvolatileについて一言いっておくか - yamasaのネタ帳
- 次期C++に導入されるメモリバリアについて解説してみる - yamasaのネタ帳
- C++0xのメモリバリアをより深く解説してみる - yamasaのネタ帳
- C/C++11 mappings to processors
- N1525: Memory-Order Rationale
- The Purpose of memory_order_consume in C++11
- (抄訳)N4215
memory_order_consume
の利用と実装に向けて[§5-6のみ] - P0371R1 Temporarily discourage
memory_order_consume
- P0439R0 Make
std::memory_order
a scoped enumeration