C++メモリモデルにおける フェンス(fence) とatomic変数/非atomic変数の関係についてメモ。
まとめ:
- C++11標準ライブラリが提供する
std::atomic_thread_fence
関数*1は、atomic変数アクセスに対してのみ意味をもつ。通常の非atomic変数には直接的な影響を与えない(間接的には影響を与えうる)。 - C++の文脈における「フェンス(fence)」は、言語仕様を記述する仮想機械(abstract machine)上の同期プリミティブにすぎない。フェンス同士またはフェンス+atomic変数操作間で「happens before関係」が定義され、直接的にはそれ以上の効果を持たない*2。
- 上記より、具体的なアーキテクチャ/コンパイラが提供するメモリバリア(memory barrier)*3とは解釈が異なる(ことが一般的なはず)。例:gcc/x86アーキテクチャの
asm volatile("mfence":::"memory")
は、非atomic変数を含む全てのメモリアクセスに直接的な影響を与える。
下記コードにおいて非atomic変数data
に対する書き込み(α)は、別スレッド上での同変数からの読み込み(β)より前に発生する(α happens before β)。
#include <atomic> int data = 0; std::atomic<bool> ready(false); // M void thread1() { data = 42; // α std::atomic_thread_fence(std::memory_order_release); // A ready.store(true, std::memory_order_relaxed); // X } void thread2() { while (!ready.load(std::memory_order_relaxed)) // Y ; std::atomic_thread_fence(std::memory_order_acquire); // B assert(data == 42); // β }
下記の3ステップより 1."α ⇒ A"+2."A ⇒ B"+3."B ⇒ β" となり、関係 "α ⇒ β" が導出される。ここで "⇒" はhappens before関係の略記とする。*4
- スレッド1上での操作αは、release fence(A)より前に順序付けられる(α sequenced before A)。(1.9/p14)
- atomic変数(M)上でのstore操作(X)+load操作(Y)を介して、両スレッド上のrelease fence(A)はacquire fence(B)と同期する(A synchronizes with B)。(29.8/p2)
- スレッド2上でのacquire fence(B)は、操作βより前に順序付けられる(B sequenced before β)。(1.9/p14)
仮に変数ready
を非atomic変数に変える(bool ready;
)と、2番目のステップが成立せず操作αと操作βの間にhappens before関係が存在しないため、変数data
でのデータ競合(data race)により未定義動作(undefined behavior)を引き起こす。またこのケースでは、当然ながら変数ready
でもデータ競合が生じる。
N3337 1.10/p5, 29.8/p2より一部引用(下線部は強調)。
5 (snip) A synchronization operation without an associated memory location is a fence and can be either an acquire fence, a release fence, or both an acquire and release fence. (snip)
2 A release fence A synchronizes with an acquire fence B if there exist atomic operations X and Y, both operating on some atomic object M, such that A is sequenced before X, X modifies M, Y is sequenced before B, and Y reads the value written by X or a value written by any side effect in the hypothetical release sequence X would head if it were a release operation.
おまけ:fence非利用版
void thread1() { data = 42; ready.store(true, std::memory_order_release); } void thread2() { while (!ready.load(std::memory_order_acquire)) ; assert(data == 42); }
関連URL
*1:std::atomic_signal_fence 関数も存在するが、本記事ではシグナルハンドラに関しては無視する。
*2:C++プログラムのセマンティクスはhappens before関係を用いて定義されるため、間接的にはプログラム全域にわたって影響を与える。この観点でのC++コンパイラ/実行環境の役目は、「仮想機械上でのセマンティクスを維持するような機械語命令列を出力/命令列を処理していくこと」と表現できる。
*3:具体的な対象によっては “メモリバリア” または “メモリフェンス(memory fence)” とも呼ぶため紛らわしい。C++標準規格では用語 "fence" のみを定義し、"(memory) barrier" や "memory fence" という用語は存在しない。
*4:本記事ではdependency-ordered before関係の存在を無視している。厳密には "⇒" はinter-thread happens before関係に対応し、最終的な "α inter-thread happens before β" から "α happens before β" を導出する。