C++
C ++ 11メモリモデル
サーチ…
備考
少なくとも1つの操作が修正( ストア操作としても知られている)である場合、同じメモリ位置にアクセスしようとする異なるスレッドがデータ競合に参加する。これらのデータ競合は 、 未定義の動作を引き起こします 。それらを回避するには、これらのスレッドがこのような競合する操作を同時に実行するのを防ぐ必要があります。
同期プリミティブ(ミューテックス、クリティカルセクションなど)は、そのようなアクセスを保護することができる。 C ++ 11で導入されたメモリモデルは、マルチスレッド環境でメモリへのアクセスを同期させる2つの新しい移植可能な方法を定義しています。アトミック操作とフェンスです。
アトミックオペレーション
アトミック・ロードおよびアトミック・ストア操作を使用することにより、所定のメモリー位置を読み書きすることが可能になりました。便宜上、これらはstd::atomic<t>
テンプレートクラスにラップされています。このクラスはt
型の値をラップしますが、今回はオブジェクトへのロードとストアはアトミックです。
テンプレートはすべてのタイプで使用できるわけではありません。利用可能なタイプは実装固有ですが、これには通常、利用可能な大部分(またはすべて)の整数タイプとポインタタイプが含まれます。したがって、 std::atomic<unsigned>
std::atomic<std::pair<bool,char>>
ほとんど使用されませんが、 std::atomic<unsigned>
とstd::atomic<std::vector<foo> *>
は使用可能になります。
アトミック操作には、次のプロパティがあります。
- すべてのアトミック操作は、未定義の動作を引き起こすことなく、複数のスレッドから同時に実行できます。
- アトミック・ロードは、アトミック・オブジェクトが構築された初期値、または何らかのアトミック・ストア操作を介してアトミック・オブジェクトに書き込まれた値のいずれかを示します。
- 同じ原子オブジェクトへのアトミックストアは、すべてのスレッドで同じ順序になります。あるスレッドがすでに何らかのアトミックストア操作の値を見ている場合、その後のアトムロード操作では、同じ値、またはそれ以降のアトミックストア操作によって格納された値が表示されます。
- アトミックなリード・モディファイ・ライト・オペレーションにより、 アトミック・ロードとアトミック・ストアは 、他のアトミック・ストアを介さずに実行できます 。たとえば、複数のスレッドからカウンタをアトミックにインクリメントすることができ、スレッド間の競合に関係なくインクリメントは失われません。
- アトミック操作は、オプションの
std::memory_order
パラメーターをstd::memory_order
ます。このパラメーターは、操作が他のメモリー位置に関してどのような追加プロパティーを持つかを定義します。
std :: memory_order | 意味 |
---|---|
std::memory_order_relaxed | 追加の制限なし |
std::memory_order_release → std::memory_order_acquire | load-acquire がstore-release によって格納された値を確認した後、 store-release 発生する前にシーケンスが store-release されてからload-acquire された後にロードが順序付けされる |
std::memory_order_consume | memory_order_acquire ようなものmemory_order_acquire が、依存負荷の場合のみです |
std::memory_order_acq_rel | load-acquire とstore-release 組み合わせる |
std::memory_order_seq_cst | 逐次整合性 |
これらのメモリオーダータグでは 、 シーケンシャル一貫性 、 リラックス 、および兄弟リリースでのリリース 取得が消費 される 、3つの異なるメモリオーダリング規律が可能です。
シーケンシャル一貫性
アトミック操作にメモリー順序が指定されていない場合、順序はデフォルトで順次整合性になります。このモードは、 std::memory_order_seq_cst
オペレーションをタグ付けすることで明示的に選択することもできます。
この順序では、メモリ操作はアトミック操作を通過できません。すべてのメモリ操作は、アトミック操作がアトミック操作およびアトミック操作が発生する前に、アトミック操作が発生する前に順次行われ、その後に順序付けられたすべてのメモリ操作が行われます。このモードはおそらく最も簡単なものですが、パフォーマンスに最大の不利益をもたらします。また、アトミック操作を超えて操作を並べ替えることを試みる可能性のあるすべてのコンパイラーの最適化を防止します。
リラックスオーダー
シーケンシャル一貫性の逆は、 緩やかなメモリ順序付けである。これはstd::memory_order_relaxed
タグで選択されます。緩和されたアトミック操作は、他のメモリ操作に制限を課さない。残っている唯一の効果は、操作自体が依然として原子的であることです。
リリースを取得する注文
アトミックストア操作にはstd::memory_order_release
タグをstd::memory_order_release
ことができ、 アトミックロード操作にはstd::memory_order_acquire
タグを付けることができます。最初の操作は(アトム)ストア・リリースと呼ばれ、2番目の操作は(アトム)ロード・アクティブと呼ばれます。
load-acquireがstore-releaseによって書き込まれた値を見ると、 store-releaseがロード取得の 前に目に見えるようになるすべてのストア操作が、 ロード取得後に順序付けられたロード操作に表示されます。
アトミックリードモディファイライトオペレーションは、累積タグstd::memory_order_acq_rel
も受け取ることができます。これにより、操作の原子負荷部分は原子のロードを獲得し、 原子ストアの部分は原子ストアの解放になります。
コンパイラーは、 アトミック・ストア・リリース操作後にストア操作を移動することはできません。また、 アトミックなロード取得 (またはロード消費 )の前に、ロード操作を移動することもできません。
原子のロード・リリースやアトミック・ストア・アクセスがないことにも注意してください。このような操作を作成しようとすると、操作が緩和されます。
リリース - 注文を消費する
この組み合わせはrelease-acquireと似ていますが、今回は原子負荷に std::memory_order_consume
タグが付けられ、 (原子的な)負荷消費操作になります。このモードは、 load-consumeによってロードされた値に応じてロードを消費した後に順序付けられたロード操作のうち、順序付けられたロード操作の中では、 release-acquireと同じです。
フェンス
フェンスはまた、メモリ操作がスレッド間で順序付けられることを可能にする。フェンスはリリースフェンスまたはフェンスを取得します。
獲得フェンスの前にリリースフェンスが発生した場合、獲得フェンスの後にシーケンスされたロードにリリースフェンスが表示される前に順序付けられたストアが格納されます。獲得フェンスの前にリリースフェンスが発生することを保証するために、緩和された原子操作を含む他の同期プリミティブを使用することができる。
メモリモデルの必要性
int x, y;
bool ready = false;
void init()
{
x = 2;
y = 3;
ready = true;
}
void use()
{
if (ready)
std::cout << x + y;
}
あるスレッドはinit()
関数を呼び出し、別のスレッド(またはシグナルハンドラ)はuse()
関数を呼び出します。 use()
関数は5
か、何もしないことを期待しているかもしれません。これは、次のような理由で必ずしも当てはまるとは限りません。
CPUは、実際に実行されるコードが次のようになるように、
init()
で発生する書き込みを並べ替えることができます:void init() { ready = true; x = 2; y = 3; }
CPUは、実際に実行されるコードが次のようになるように、
use()
で発生する読み取りを並べ替えることがuse()
。void use() { int local_x = x; int local_y = y; if (ready) std::cout << local_x + local_y; }
最適化C ++コンパイラは、同様の方法でプログラムを並べ替えることにします。
このような並べ替えは、スレッドがinit()
とuse()
呼び出しをインターリーブできないため、単一スレッドで実行されているプログラムの動作を変更することはできません。一方、マルチスレッド設定では、他のスレッドによって実行された書き込みの一部が見えるかもしれませんが、 use()
はready==true
、 x
またはy
またはその両方のガーベッジを見ることがあります。
C ++メモリモデルは、マルチスレッドプログラムが期待どおりに動作できるように、プログラマがどのリオーダリング操作が許可されているかどうかを指定することを可能にします。上記の例は、スレッドセーフな方法で次のように書き直すことができます。
int x, y;
std::atomic<bool> ready{false};
void init()
{
x = 2;
y = 3;
ready.store(true, std::memory_order_release);
}
void use()
{
if (ready.load(std::memory_order_acquire))
std::cout << x + y;
}
ここで、 init()
は原子ストアの解放操作を実行します。これは、値を格納するだけでなく、 true
の中にready
するだけでなく、それはそれの前に配列決定される書き込み操作の前に、この操作を移動することはできませんコンパイラに指示します。
use()
関数は、 アトミックなロード取得操作を行います。これはready
の現在の値を読み込み、コンパイラがアトミックなロードを取得する 前に シーケンシングされた読み取り操作を配置することを禁止します。
これらのアトミック操作はまた、コンパイラに、CPUに通知して不要な並べ替えを控えるために必要なハードウェア命令を置くようにします。
アトミック・ストア・リリースはアトミック・ロード取得と同じメモリ位置にあるため 、 ロード・アクイジション・オペレーションがストア・リリース操作によって書き込まれた値を参照すると、 init()
'によって実行されるすべての書き込みは、そのstore-releaseより前のスレッドは、 load use()
のスレッドがロード獲得後に実行するロードに対して可視になります。つまり、 use()
がready==true
見れば、 x==2
とy==3
保証します。
コンパイラとCPUは、まだx
に書き込む前にy
に書き込むことが許可されています。同様に、 use()
これらの変数からの読み込みは、任意の順序で実行できます。
フェンスの例
上記の例はフェンスと緩やかな原子操作で実装することもできます:
int x, y;
std::atomic<bool> ready{false};
void init()
{
x = 2;
y = 3;
atomic_thread_fence(std::memory_order_release);
ready.store(true, std::memory_order_relaxed);
}
void use()
{
if (ready.load(std::memory_order_relaxed))
{
atomic_thread_fence(std::memory_order_acquire);
std::cout << x + y;
}
}
アトミック・ロード・オペレーションがアトミック・ストアによって書き込まれた値を見ると、ストアはロードの前に発生し、フェンスも同様です。リリース・フェンスは、取得フェンスの前に発生し、リリース・フェンスが表示される前のx
およびy
取得フェンスに続くstd::cout
ステートメントにstd::cout
ます。
フェンスは、取得、解放、またはその他の同期操作の総数を減らすことができれば有益です。例えば:
void block_and_use()
{
while (!ready.load(std::memory_order_relaxed))
;
atomic_thread_fence(std::memory_order_acquire);
std::cout << x + y;
}
block_and_use()
関数は、緩やかな原子負荷の助けを借りてready
フラグがセットされるまで回転します。次に、単一の獲得フェンスを使用して、必要なメモリ順序を提供する。