Namespaces
Variants

memory_order

From cppreference.net
ヘッダーで定義 <stdatomic.h>
enum memory_order

{
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst

} ;
(C11以降)

memory_order は、通常の非アトミックメモリアクセスを含むメモリアクセスが、アトミック操作を中心にどのように順序付けられるかを指定します。マルチコアシステムにおいて制約がない場合、複数のスレッドが同時に複数の変数を読み書きすると、あるスレッドが値の変化を別のスレッドが書き込んだ順序とは異なる順序で観測することがあります。実際、変化の見かけ上の順序は複数のリーダースレッド間でも異なる可能性があります。メモリモデルによって許可されるコンパイラ変換により、ユニプロセッサシステムでも同様の効果が発生することがあります。

言語およびライブラリにおけるすべてのアトミック操作のデフォルト動作は、 language で定義されている sequentially consistent ordering (後述の議論を参照)を提供します。このデフォルト設定はパフォーマンスに影響を与える可能性がありますが、ライブラリのアトミック操作には追加の memory_order 引数を指定でき、その操作に対してコンパイラとプロセッサが強制すべき、アトミック性を超えた正確な制約を規定することができます。

目次

定数

ヘッダーで定義 <stdatomic.h>
説明
memory_order_relaxed 緩和された操作:他の読み書きに対して同期や順序制約は課されず、この操作のアトミック性のみが保証される(下記の 緩和順序付け を参照)。
memory_order_consume
(C++26で非推奨)
このメモリ順序を持つロード操作は、影響を受けるメモリ位置に対して コンシューム操作 を実行する:現在ロードされた値に依存する現在のスレッド内の読み書きは、このロードの前に並べ替えることができない。同じアトミック変数を解放する他のスレッド内のデータ依存変数への書き込みは、現在のスレッドで可視となる。ほとんどのプラットフォームでは、これはコンパイラの最適化にのみ影響する(下記の 解放-消費順序付け を参照)。
memory_order_acquire このメモリ順序を持つロード操作は、影響を受けるメモリ位置に対して アクワイア操作 を実行する:現在のスレッド内の読み書きは、このロードの前に並べ替えることができない。同じアトミック変数を解放する他のスレッド内のすべての書き込みは、現在のスレッドで可視となる(下記の 解放-取得順序付け を参照)。
memory_order_release このメモリ順序を持つストア操作は リリース操作 を実行する:現在のスレッド内の読み書きは、このストアの後に並べ替えることができない。現在のスレッド内のすべての書き込みは、同じアトミック変数を取得する他のスレッドで可視となり(下記の 解放-取得順序付け を参照)、アトミック変数への依存関係を持つ書き込みは、同じアトミック変数を消費する他のスレッドで可視となる(下記の 解放-消費順序付け を参照)。
memory_order_acq_rel このメモリ順序を持つ読み込み-修正-書き込み操作は、 アクワイア操作 リリース操作 の両方である。現在のスレッド内のメモリ読み書きは、ロードの前に、またストアの後に並べ替えることができない。同じアトミック変数を解放する他のスレッド内のすべての書き込みは、修正前に可視となり、修正は同じアトミック変数を取得する他のスレッドで可視となる。
memory_order_seq_cst このメモリ順序を持つロード操作は アクワイア操作 を実行し、ストアは リリース操作 を実行し、読み込み-修正-書き込みは アクワイア操作 リリース操作 の両方を実行する。さらに、すべてのスレッドがすべての修正を同じ順序で観察する単一の全順序が存在する(下記の 逐次一貫性順序付け を参照)。

緩和された順序付け

memory_order_relaxed でタグ付けされたアトミック操作は、同期操作ではありません。これらは並行メモリアクセス間での順序を強制しません。アトミック性と変更順序の一貫性のみを保証します。

例えば、 x y が初期値ゼロの場合、

// スレッド 1:
r1 = atomic_load_explicit ( y, memory_order_relaxed ) ; // A
atomic_store_explicit ( x, r1, memory_order_relaxed ) ; // B
// スレッド 2:
r2 = atomic_load_explicit ( x, memory_order_relaxed ) ; // C
atomic_store_explicit ( y, 42 , memory_order_relaxed ) ; // D
r1 == r2 == 42 を生成することが許可されます。なぜなら、スレッド1内ではAがBに対して シーケンス前に あり、スレッド2内ではCがDに対して シーケンス前に あるものの、Dがyの変更順序でAより前に現れること、およびBがxの変更順序でCより前に現れることを妨げるものはないからです。スレッド1のロードAに対してyに対するDの副作用が可視となり、同時にスレッド2のロードCに対してxに対するBの副作用が可視となる可能性があります。特に、コンパイラの並べ替えまたは実行時において、スレッド2でDがCより前に完了する場合に発生する可能性があります。

緩和されたメモリ順序の典型的な使用例は、参照カウンタなどのカウンタのインクリメントです。これはアトミック性のみを必要とし、順序付けや同期を必要としないためです。

Release-Consume順序付け

スレッドAにおけるアトミックストアが memory_order_release でタグ付けされ、スレッドBにおける同じ変数からのアトミックロードが memory_order_consume でタグ付けされ、かつスレッドBのロードがスレッドAのストアによって書き込まれた値を読み取る場合、スレッドAのストアはスレッドBのロードに対して 依存順序付けされる

アトミックストアの前に発生した(非アトミックおよび緩和アトミックな)すべてのメモリ書き込み操作は、スレッドAの観点から見て、 happened-before 関係にあるものが、 visible side-effects として、スレッドBにおいてロード操作が carries dependency する範囲内で可視となる。つまり、アトミックロードが完了すると、スレッドBでロードから得られた値を使用する演算子や関数は、スレッドAがメモリに書き込んだ内容を確実に認識できるようになる。

同期は、同じアトミック変数を 解放 するスレッドと 取得 するスレッドの間でのみ確立されます。他のスレッドは、同期されたスレッドのいずれかまたは両方とは異なるメモリアクセスの順序を観察する可能性があります。

DEC Alphaを除くすべての主流CPUでは、依存関係の順序付けは自動的に行われます。この同期モードでは追加のCPU命令は発行されず、特定のコンパイラ最適化のみが影響を受けます(例えば、コンパイラは依存関係チェーンに関与するオブジェクトに対して投機的ロードを実行することが禁止されます)。

この順序付けの典型的な使用例には、頻繁に書き込まれない並行データ構造(ルーティングテーブル、設定、セキュリティポリシー、ファイアウォールルールなど)への読み取りアクセスや、ポインタを介した公開を行うパブリッシャー-サブスクライバー状況が含まれます。つまり、プロデューサーがコンシューマーが情報にアクセスできるポインタを公開する場合、プロデューサーがメモリに書き込んだ他のすべてをコンシューマーに可視化する必要はありません(これは弱い順序付けのアーキテクチャでは高コストな操作となる可能性があります)。このようなシナリオの例は rcu_dereference です。

現在(2015年2月時点)既知のプロダクションコンパイラは依存性チェーンを追跡しないことに注意してください:consume操作はacquire操作に昇格されます。

リリースシーケンス

あるアトミック変数がストアリリースされ、その同じアトミック変数に対して複数のスレッドがread-modify-write操作を実行する場合、「リリースシーケンス」が形成されます:同じアトミック変数に対してread-modify-writeを実行するすべてのスレッドは、たとえそれらが memory_order_release セマンティクスを持たなくても、最初のスレッドおよび互いに同期します。これにより、個々のコンシューマスレッド間の不必要な同期を課すことなく、単一プロデューサ - 複数コンシューマの状況が可能になります。

Release-Acquire順序付け

スレッドAにおけるアトミックストアが memory_order_release でタグ付けされ、スレッドBにおける同じ変数からのアトミックロードが memory_order_acquire でタグ付けされ、かつスレッドBのロードがスレッドAのストアによって書き込まれた値を読み取る場合、スレッドAのストアはスレッドBのロードと synchronizes-with 関係を確立します。

スレッドAの観点からアトミックストアの前に発生したすべてのメモリ書き込み(非アトミックおよび緩和アトミックを含む)は、 happened-before 関係により、スレッドBにおいて visible side-effects となります。つまり、アトミックロードが完了すると、スレッドBはスレッドAがメモリに書き込んだすべての内容を確実に認識します。この保証は、Bが実際にAが格納した値、またはリリースシーケンスでそれ以降の値を返す場合にのみ有効です。

同期は、同じアトミック変数を releasing するスレッドと acquiring するスレッドの間でのみ確立されます。他のスレッドは、同期されたスレッドの一方または両方とは異なるメモリアクセスの順序を観測する可能性があります。

強く順序付けられたシステム(x86、SPARC TSO、IBMメインフレームなど)では、リリース-取得順序付けはほとんどの操作で自動的に行われます。この同期モードでは追加のCPU命令は発行されず、特定のコンパイラ最適化のみが影響を受けます(例えば、コンパイラは非アトミックストアをアトミックストア-リリースを超えて移動したり、非アトミックロードをアトミックロード-取得より早く実行したりすることが禁止されます)。弱く順序付けられたシステム(ARM、Itanium、PowerPC)では、特別なCPUロードまたはメモリフェンス命令が使用されます。

相互排他ロック、例えば mutexes atomic spinlocks は、release-acquire同期の一例です:スレッドAによってロックが解放され、スレッドBによって取得されるとき、クリティカルセクション内(解放前)でスレッドAのコンテキストで発生したすべてのことは、同じクリティカルセクションを実行しているスレッドB(取得後)に対して可視である必要があります。

逐次一貫性順序付け

memory_order_seq_cst でタグ付けされたアトミック操作は、リリース/アクquire順序と同じ方法でメモリを順序付けるだけでなく(1つのスレッドでのストアに 先行発生 したすべてが、ロードを行ったスレッドで 可視的な副作用 となる)、このようにタグ付けされたすべてのアトミック操作の 単一の全変更順序 も確立します。

形式的には、

memory_order_seq_cst 操作 B は、アトミック変数 M から読み込む際に、以下のいずれかを観察します:

  • 単一の全順序においてBよりも前に現れる、Mを変更した最後の操作Aの結果、
  • または、そのようなAが存在する場合、BはAに happen-before 関係せず、かつ memory_order_seq_cst ではないMに対する何らかの変更の結果を観測する可能性がある、
  • または、そのようなAが存在しない場合、Bは memory_order_seq_cst ではない無関係なMの変更の結果を観測する可能性がある。

memory_order_seq_cst 操作XがBに対して sequenced-before 関係にある場合、Bは以下のいずれかを観測します:

  • Xが単一全順序において前に現れる、Mの最後の memory_order_seq_cst 修飾、
  • Mの変更順序で後に現れる、Mの無関係な変更。

Mに対する原子操作のペアAとB(Aは書き込み、BはMの値を読み取り)において、2つの memory_order_seq_cst atomic_thread_fence XとYが存在し、AがXに対して sequenced-before の関係にあり、YがBに対して sequenced-before の関係にあり、かつ単一全順序においてXがYより前に現れる場合、Bは以下のいずれかを観測する:

  • Aの効果、
  • Mの変更順序においてAの後に現れる、Mの無関係な変更。

Mの変更順序において、AとBと呼ばれるMの一対のアトミックな変更について、BがAの後に発生するのは以下の場合です。

  • Aが sequenced-before 関係にあり、かつ単一全順序においてXがBより前に現れるような memory_order_seq_cst atomic_thread_fence Xが存在する、あるいは
  • YがBに対して sequenced-before 関係にあり、かつ単一全順序においてAがYより前に現れるような memory_order_seq_cst atomic_thread_fence Yが存在する、あるいは
  • AがXに対して sequenced-before 関係にあり、YがBに対して sequenced-before 関係にあり、かつ単一全順序においてXがYより前に現れるような memory_order_seq_cst atomic_thread_fence s XおよびYが存在する。

これは以下を意味することに注意してください:

1) memory_order_seq_cst でタグ付けされていないアトミック操作が登場すると、逐次一貫性は失われます。
2) シーケンシャル一貫性フェンスは、一般的なケースではアトミック操作に対してではなく、フェンス自体に対してのみ全順序を確立する( sequenced-before happens-before とは異なり、スレッド間の関係ではない)。

シーケンシャルな順序付けは、すべてのコンシューマがすべてのプロデューサのアクションを同じ順序で観測する必要がある、マルチプロデューサ-マルチコンシューマの状況で必要となる場合があります。

トータル・シーケンシャル・オーダリングは、すべてのマルチコアシステムにおいて完全なメモリフェンスCPU命令を必要とします。これは、影響を受けるメモリアクセスがすべてのコアに伝播することを強制するため、パフォーマンスのボトルネックとなる可能性があります。

volatileとの関係

実行スレッド内では、 volatile lvalues を通じたアクセス(読み取りおよび書き込み)は、同一スレッド内のシーケンスポイントで区切られた観測可能な副作用(他のvolatileアクセスを含む)を超えて並べ替えられることはありません。しかし、この順序は他のスレッドから観測されることが保証されません。なぜならvolatileアクセスはスレッド間同期を確立しないからです。

さらに、volatileアクセスはアトミックではありません(同時読み書きは データ競合 です)また、メモリ順序を保証しません(非volatileメモリアクセスはvolatileアクセスの周りで自由に並べ替えられる可能性があります)。

注目すべき例外はVisual Studioであり、デフォルト設定では、すべてのvolatile書き込みはrelease semanticsを持ち、すべてのvolatile読み取りはacquire semanticsを持ちます( Microsoft Docs )。そのため、volatileはスレッド間同期に使用されることがあります。標準的な volatile セマンティクスはマルチスレッドプログラミングには適用できませんが、 signal ハンドラとの通信(同じスレッドで実行される場合)には十分であり、特に sig_atomic_t 変数に適用された場合に有効です。コンパイラオプション /volatile:iso を使用すると、標準に準拠した動作に戻すことができ、これはターゲットプラットフォームがARMの場合のデフォルト設定です。

参考文献

  • C23規格 (ISO/IEC 9899:2024):
  • 7.17.1/4 memory_order (p: TBD)
  • 7.17.3 順序と一貫性 (p: TBD)
  • C17規格 (ISO/IEC 9899:2018):
  • 7.17.1/4 memory_order (p: 200)
  • 7.17.3 順序と一貫性 (p: 201-203)
  • C11標準 (ISO/IEC 9899:2011):
  • 7.17.1/4 memory_order (p: 273)
  • 7.17.3 順序と一貫性 (p: 275-277)

関連項目

C++ ドキュメント for memory order
翻訳内容: - 「C++ documentation」→「C++ ドキュメント」 - 「for」→「for」(C++用語の前にあるためそのまま保持) - 「memory order」→「memory order」(C++固有の用語のため翻訳せず保持) HTMLタグ、属性、およびC++固有の用語はすべて原文のまま保持されています。

外部リンク

1. MOESIプロトコル
2. x86-TSO: x86マルチプロセッサのための厳密かつ実用的なプログラマモデル P. Sewell 他, 2010
3. ARMおよびPOWER緩和メモリモデルのチュートリアル入門 P. Sewell 他, 2012
4. MESIF: ポイントツーポイント相互接続のための2ホップキャッシュ一貫性プロトコル J.R. Goodman, H.H.J. Hum, 2009
5. メモリモデル Russ Cox, 2021