Namespaces
Variants

std:: memory_order

From cppreference.net
Concurrency support library
Threads
(C++11)
(C++20)
this_thread namespace
(C++11)
(C++11)
Cooperative cancellation
Mutual exclusion
Generic lock management
Condition variables
(C++11)
Semaphores
Latches and Barriers
(C++20)
(C++20)
Futures
(C++11)
(C++11)
(C++11)
Safe reclamation
Hazard pointers
Atomic types
(C++11)
(C++20)
Initialization of atomic types
(C++11) (deprecated in C++20)
(C++11) (deprecated in C++20)
Memory ordering
memory_order
(C++11)
(C++11) (deprecated in C++26)
Free functions for atomic operations
Free functions for atomic flags
ヘッダーで定義 <atomic>
enum memory_order

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

} ;
(C++11以降)
(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 ;
(C++20以降)

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

C++標準ライブラリにおける全てのアトミック操作のデフォルト動作は、 逐次一貫性順序 を提供します(以下の議論を参照)。このデフォルト設定はパフォーマンスに悪影響を及ぼす可能性がありますが、ライブラリのアトミック操作には追加の std::memory_order 引数を指定することで、原子性を超えた正確な制約をコンパイラとプロセッサに強制させることができます。

目次

定数

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

形式的記述

スレッド間同期とメモリ順序付けは、異なる実行スレッド間で式の 評価 副作用 がどのように順序付けられるかを決定します。これらは以下の用語で定義されます:

シーケンス前に

同一スレッド内では、評価Aが評価Bに対して sequenced-before の関係を持つ場合があり、これは evaluation order で説明されています。

依存関係の伝播

同一スレッド内で、評価Aが評価Bに対して 先行順序付け されている場合、以下のいずれかに該当するときは、AがBに依存関係を伝播する(つまり、BがAに依存する)可能性がある:

1) Aの値がBのオペランドとして使用される場合、 ただし以下を除く
a) Bが std::kill_dependency の呼び出しである場合、
b) Aが組み込み演算子 && || ?: 、または , の左オペランドである場合。
2) AがスカラーオブジェクトMに書き込み、BがMから読み取る場合。
3) Aが別の評価Xに依存関係を伝播し、XがBに依存関係を伝播する場合。
(C++26まで)

変更順序

特定のアトミック変数に対するすべての変更は、そのアトミック変数に固有の全順序で発生します。

以下の4つの要件は、すべてのアトミック操作に対して保証されています:

1) Write-write coherence : あるアトミック変数Mを変更する評価A(書き込み)が、Mを変更する評価Bよりも happens-before 関係にある場合、AはMの modification order においてBよりも前に現れる。
2) Read-read coherence : あるアトミック変数Mに対する値計算A(読み取り)が、Mに対する値計算Bよりも happens-before の関係にあり、かつAの値がMに対する書き込みXに由来する場合、Bの値はXによって格納された値か、あるいはMの modification order においてXよりも後に現れる副作用Yによって格納された値のいずれかとなる。
3) 読み書き一貫性 : あるアトミック変数Mの値計算A(読み取り)がMに対する操作B(書き込み)に対して happens-before 関係にある場合、Aの値はMの modification order においてBより前に現れる副作用(書き込み)Xに由来する。
4) Write-read coherence : アトミックオブジェクトMに対する副作用(書き込み)Xが、Mの値計算(読み取り)Bに対して happens-before 関係にある場合、評価Bはその値をX、またはMの変更順序においてXに続く副作用Yから取得しなければならない。

解放シーケンス

アトミックオブジェクト M に対して release operation A が実行された後、M の modification order の中で最も長い連続部分列であり、以下から構成されるもの:

1) Aを実行したのと同じスレッドによって実行される書き込み。
(until C++20)
2) 任意のスレッドによってMに対して行われるアトミックなread-modify-write操作。

Aによって先頭とされるリリースシーケンス として知られています。

同期対象

スレッドAにおけるアトミックストアが release操作 であり、スレッドBにおける同じ変数からのアトミックロードが acquire操作 であり、かつスレッドBのロードがスレッドAのストアによって書き込まれた値を読み取る場合、スレッドAのストアはスレッドBのロードと synchronizes-with の関係が成立します。

また、一部のライブラリ呼び出しは、他のスレッド上の別のライブラリ呼び出しと synchronize-with 関係を持つように定義されている場合があります。

依存関係順序付け先行

スレッド間において、以下のいずれかが真である場合、評価Aは評価Bに対して 依存関係順序付け先行 する:

1) Aが何らかのアトミック変数Mに対して 解放操作 を実行し、かつ異なるスレッドにおいてBが同じアトミック変数Mに対して 消費操作 を実行し、かつBがAによって書き込まれた値を読み取る場合 (解放シーケンスの先頭による任意の部分) (C++20まで)
2) AがXに対して依存関係順序付け先行し、かつXがBに対して依存関係を伝播する場合。
(C++26まで)

スレッド間 happens-before

スレッド間では、以下のいずれかが真である場合、評価Aは inter-thread happens before 評価Bとなります:

1) Aは synchronizes-with Bします。
2) Aは dependency-ordered before Bです。
3) ある評価Xと synchronizes-with 関係にあり、かつXがBに対して sequenced-before 関係にある。
4) Aは sequenced-before 関係にある何らかの評価Xであり、かつXが inter-thread happens-before 関係にあるBである。
5) ある評価Aが inter-thread happens-before 関係にあり、かつXが inter-thread happens-before 関係にある場合。


Happens-before

スレッドに関係なく、以下のいずれかが真である場合、評価Aは評価Bに対して happens-before する:

1) AがBに対して sequenced-before である。
2) AがBに対して inter-thread happens before する。

実装は、 happens-before 関係が非循環的であることを保証する必要がある(必要に応じて追加の同期を導入することで、これはconsume操作が関与する場合にのみ必要となる可能性がある。詳細は Batty et al を参照)。

一方の評価がメモリ位置を変更し、もう一方の評価が同じメモリ位置を読み取りまたは変更し、かつ少なくとも一方の評価がアトミック操作でない場合、これら2つの評価の間に happens-before 関係が存在しない限り、プログラムの動作は未定義となる(プログラムは データ競合 を持つ)。

Simply happens-before

スレッドに関係なく、以下のいずれかが真である場合、評価Aは評価Bに対して simply happens-before する:

1) AがBに対して sequenced-before である。
2) AがBに対して synchronizes-with する。
3) AがXに対して simply happens-before し、かつXがBに対して simply happens-before する。

注:consume操作がない場合、 simply happens-before 関係と happens-before 関係は同じである。

(C++20以降)
(C++26まで)

Happens-before

スレッドに関係なく、以下のいずれかが真である場合、評価Aは評価Bに対して happens-before する:

1) AがBに対して sequenced-before である。
2) AがBに対して synchronizes-with する。
3) AがXに対して happens-before し、かつXがBに対して happens-before する。
(C++26以降)

強く発生する順序 (Strongly happens-before)

スレッドに関係なく、以下のいずれかが真である場合、評価Aは評価Bに対して strongly happens-before します:

1) AはBに対して sequenced-before である。
2) AはBに対して synchronizes-with する。
3) AはXに対して strongly happens-before し、XはBに対して strongly happens-before する。
(C++20まで)
1) AはBに対して sequenced-before である。
2) AはBに対して synchronizes with し、かつAとBの両方が逐次一貫性アトミック操作である。
3) AはXに対して sequenced-before であり、XはYに対して simply (C++26まで) happens-before し、YはBに対して sequenced-before である。
4) AはXに対して strongly happens-before し、XはBに対して strongly happens-before する。

注: 非公式には、AがBに対して strongly happens-before する場合、Aはすべての文脈でBよりも前に評価されているように見える。

注: strongly happens-before はconsume操作を除外する。

(C++26まで)
(C++20以降)

可視副作用

スカラーMに対する副作用A(書き込み)は、以下の両方が真である場合、Mに対する値計算B(読み取り)に関して 可視 である:

1) Aは happens-before Bとなります。
2) AがXに happens-before し、かつXがBに happens-before するような、Mに対する他の副作用Xは存在しない。

副作用Aが値計算Bに対して可視である場合、Mに対する副作用の最長連続部分集合で、 変更順序 においてBがそれに 先行発生 しないものは、 可視副作用列 として知られる(Bによって決定されるMの値は、これらの副作用のいずれかによって格納された値となる)。

注意: スレッド間同期は、データ競合の防止(happens-before関係の確立による)と、どの副作用がどの条件下で可視化されるかを定義することに帰着します。

Consume操作

memory_order_consume またはそれより強いメモリ順序でのアトミックロードはコンシューム操作です。 std::atomic_thread_fence はコンシューム操作よりも強い同期要件を課すことに注意してください。

Acquire操作

memory_order_acquire またはそれ以上のメモリ順序を持つアトミックロードはacquire操作です。 Mutex に対する lock() 操作もacquire操作です。 std::atomic_thread_fence はacquire操作よりも強い同期要件を課すことに注意してください。

リリース操作

memory_order_release またはそれ以上のメモリ順序を持つアトミックストアはリリース操作です。 Mutex unlock() 操作もリリース操作です。 std::atomic_thread_fence はリリース操作よりも強い同期要件を課すことに注意してください。

説明

緩和された順序付け

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

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

// スレッド1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// スレッド2:
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

r1 == r2 == 42 が生成されることが許容されます。なぜなら、スレッド1内ではAがBより sequenced-before であり、スレッド2内ではCがDより sequenced before であるものの、 y の変更順序においてDがAより前に現れることや、 x の変更順序においてBがCより前に現れることを妨げるものは何もないからです。スレッド1のロードAに対して y へのDの副作用が見える可能性があり、同時にスレッド2のロードCに対して x へのBの副作用が見える可能性があります。特に、コンパイラのリオーダリングまたは実行時において、スレッド2でDがCより先に完了する場合にこれが発生し得ます。

緩和されたメモリモデルであっても、out-of-thin-air値が自身の計算に循環依存することは許可されません。例えば、 x y が初期値0の場合、

// Thread 1:
r1 = y.load(std::memory_order_relaxed);
if (r1 == 42)
    x.store(r1, std::memory_order_relaxed);
// Thread 2:
r2 = x.load(std::memory_order_relaxed);
if (r2 == 42)
    y.store(42, std::memory_order_relaxed);

r1 == r2 == 42 を生成することは許可されません。なぜなら、 42 y へのストアは、 x へのストアが 42 をストアする場合にのみ可能であり、これは y へのストアが 42 をストアすることに循環依存しているからです。C++14までは、仕様上これは技術的に許可されていましたが、実装者には推奨されていなかったことに注意してください。

(C++14以降)

緩和されたメモリ順序の典型的な使用例は、カウンタのインクリメントです。例えば std::shared_ptr の参照カウンタなどが該当します。これはアトミック性のみを要求し、順序付けや同期は必要としないためです(ただし、 std::shared_ptr カウンタのデクリメントは、デストラクタとの間でacquire-release同期を必要とすることに注意してください)。

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<int> cnt = {0};
void f()
{
    for (int n = 0; n < 1000; ++n)
        cnt.fetch_add(1, std::memory_order_relaxed);
}
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n)
        v.emplace_back(f);
    for (auto& t : v)
        t.join();
    std::cout << "Final counter value is " << cnt << '\n';
}

出力:

Final counter value is 10000

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ロードまたはメモリフェンス命令が使用されます。

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

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string*> ptr;
int data;
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // never fires
    assert(data == 42); // never fires
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

以下の例は、リリースシーケンスを使用して3つのスレッド間で推移的なrelease-acquire順序付けを示しています。

#include <atomic>
#include <cassert>
#include <thread>
#include <vector>
std::vector<int> data;
std::atomic<int> flag = {0};
void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
}
void thread_2()
{
    int expected = 1;
    // memory_order_relaxed is okay because this is an RMW,
    // and RMWs (with any ordering) following a release form a release sequence
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed))
    {
        expected = 1;
    }
}
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    // if we read the value 2 from the atomic flag, we see 42 in the vector
    assert(data.at(0) == 42); // will never fire
}
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}

Release-Consume順序付け

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

スレッドAの観点からアトミックストアの前に 発生した すべてのメモリ書き込み(非アトミックおよび緩和アトミック)は、ロード操作が 依存を伝播する スレッドB内の操作において 可視的な副作用 となる。つまり、アトミックロードが完了すると、ロードから得られた値を使用するスレッドB内の演算子および関数は、スレッドAがメモリに書き込んだ内容を確実に認識する。

同期は、同じアトミック変数を 解放する スレッドと 消費する スレッドの間でのみ確立される。他のスレッドは、同期されたスレッドの一方または両方とは異なるメモリアクセスの順序を見ることがある。

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

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

詳細な依存チェーン制御については、 std::kill_dependency および [[ carries_dependency ]] も参照のこと。

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

(C++26まで)

release-consume順序付けの仕様は改訂中であり、 memory_order_consume の使用は一時的に推奨されません。

(C++17以降)
(C++26まで)

release-consume順序付けはrelease-acquire順序付けと同じ効果を持ち、非推奨となりました。

(C++26以降)

この例は、ポインタを介した公開における依存順序同期を示しています:整数データは文字列へのポインタとデータ依存関係を持たないため、コンシューマ側でのその値は未定義となります。

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string*> ptr;
int data;
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr
    assert(data == 42); // may or may not fire: data does not carry dependency from ptr
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


逐次一貫性順序付け

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

形式的には、

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

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

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

  • 単一の全順序においてXより前に現れる、Mに対する最後の memory_order_seq_cst 変更、
  • Mの変更順序において後に現れる、Mに対する無関係な変更。

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

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

Mに対するアトミック変更のペアAとBにおいて、以下の場合にBはMの変更順序においてAの後に発生する:

  • Aが sequenced-before 関係にあり、単一の全順序においてXがBより前に現れる memory_order_seq_cst std::atomic_thread_fence Xが存在する場合、
  • または、YがBに対して sequenced-before 関係にあり、単一の全順序においてAがYより前に現れる memory_order_seq_cst std::atomic_thread_fence Yが存在する場合、
  • または、AがXに対して sequenced-before 関係にあり、YがBに対して sequenced-before 関係にあり、単一の全順序においてXがYより前に現れる memory_order_seq_cst std::atomic_thread_fence XとYが存在する場合。

これは以下のことを意味する:

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

あるアトミックオブジェクトMに対するアトミック操作Aが、Mに対する別のアトミック操作Bに対して coherence-ordered-before となるのは、以下のいずれかが真である場合です:

1) Aが変更操作であり、BがAによって格納された値を読み取る場合、
2) AがMの modification order においてBより前にある場合、
3) Aがアトミック変更Xによって格納された値を読み取り、Xが modification order においてBより前にあり、かつAとBが同じアトミックread-modify-write操作でない場合、
4) AがXに対して coherence-ordered-before であり、XがBに対して coherence-ordered-before である場合。

すべての memory_order_seq_cst 操作(フェンスを含む)に対して単一の全順序Sが存在し、以下の制約を満たします:

1) AとBが memory_order_seq_cst 操作であり、AがBに対して 強く happens-before する場合、SにおいてAはBより前にある、
2) オブジェクトMに対するアトミック操作AとBのペアについて、AがBに対して coherence-ordered-before である場合:
a) AとBがともに memory_order_seq_cst 操作である場合、SにおいてAはBより前にある、
b) Aが memory_order_seq_cst 操作であり、Bが memory_order_seq_cst フェンスYに対して happens-before する場合、SにおいてAはYより前にある、
c) memory_order_seq_cst フェンスXがAに対して happens-before し、Bが memory_order_seq_cst 操作である場合、SにおいてXはBより前にある、
d) memory_order_seq_cst フェンスXがAに対して happens-before し、Bが memory_order_seq_cst フェンスYに対して happens-before する場合、SにおいてXはYより前にある。

この形式的定義により以下が保証されます:

1) 単一の全順序は、任意のアトミックオブジェクトの modification order と整合性がある、
2) memory_order_seq_cst ロードは、最後の memory_order_seq_cst 変更から、または先行する memory_order_seq_cst 変更に対して happen-before しない非 memory_order_seq_cst 変更から値を取得する。

単一の全順序は happens-before と整合性がない可能性があります。これにより、一部のCPUにおける memory_order_acquire memory_order_release のより効率的な実装が可能になります。 memory_order_acquire memory_order_release memory_order_seq_cst と混在する場合、驚くべき結果を生み出すことがあります。

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

// Thread 1:
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// Thread 2:
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// Thread 3:
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F

r1 == 1 && r2 == 3 && r3 == 0 という結果を生成することが許可されます。ここでAはCに対して happens-before しますが、Cは memory_order_seq_cst の単一の全順序C-E-F-AにおいてAより前にあります( Lahav et al を参照)。

注意点:

1) memory_order_seq_cst でタグ付けされていないアトミック操作が含まれると、プログラムに対する逐次一貫性の保証は失われます、
2) 多くの場合、 memory_order_seq_cst アトミック操作は、同じスレッドによって実行される他のアトミック操作に対して並べ替え可能です。
(C++20以降)

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

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

この例は、逐次一貫性の順序付けが必要な状況を示しています。他の順序付けでは、スレッド c d がアトミック変数 x y への変更を逆の順序で観測する可能性があるため、アサートがトリガーされる可能性があります。

#include <atomic>
#include <cassert>
#include <thread>
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst))
        ++z;
}
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst))
        ++z;
}
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0); // will never happen
}

volatile との関係

実行スレッド内では、 volatile glvalues を通じたアクセス(読み書き)は、同じスレッド内で sequenced-before または sequenced-after の関係にある観測可能な副作用(他のvolatileアクセスを含む)を越えて並べ替えることはできません。しかし、この順序は他のスレッドから観測されることが保証されていません。なぜならvolatileアクセスはスレッド間同期を確立しないからです。

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

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

関連項目

C documentation for memory order
日本語訳:
C documentation for memory order
翻訳結果: - "C documentation for memory order" は「memory order の C ドキュメント」と訳しました - HTMLタグ、属性、C++固有用語(memory order)は翻訳せず、元のフォーマットを保持しています - 技術文書としての正確性と専門性を維持しています

外部リンク

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