名前空間
変種
操作

std::memory_order

提供: cppreference.com
< cpp‎ | atomic
 
 
 
ヘッダ <atomic> で定義
typedef enum memory_order {

    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst

} memory_order;
(1) (C++11およびそれ以降)
(C++20以前)
enum class memory_order : /*unspecified*/ {

    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;
(2) (C++20およびそれ以降)

std::memory_order は、通常の非アトミックなメモリアクセスがアトミック操作の周りでどのように順序付けられるかを指定します。 マルチコアシステムにおいて、いかなる制約もなければ、複数のスレッドがいくつかの変数を同時に読み書きしたとき、あるスレッドが書き込んだ順序とは異なる順序で別のスレッドからその値の変更が観察される可能性があります。 実際、変更が見える順序は複数の読み込みスレッド間で異なる可能性さえあります。 シングルコアシステムにおいてさえも、メモリモデルによって許されているコンパイラの変換のために、同様の効果が発生する可能性があります。

ライブラリのすべてのアトミック操作が提供するデフォルトの動作は、逐次一貫性順序付けです (後述の説明を参照してください)。 このデフォルトは性能的に不利ですが、ライブラリのアトミック操作では、アトミック性だけではなく、その操作に対してコンパイラおよびプロセッサが強制しなければならない正確な制約を指定する、追加の std::memory_order 引数を渡すことができます。

目次

[編集] 定数

ヘッダ <atomic> で定義
説明
memory_order_relaxed 緩和された操作。 他の読み書きに対して課される順序制約や同期はなく、この操作のアトミック性のみが保証されます (後述の緩和された順序付けを参照してください)。
memory_order_consume このメモリ順序を持つロード操作は、影響を受けるメモリ位置に対して消費操作を行います。 現在ロード中の値に依存する現在のスレッド内の読み書きは、このロードの前に順序変更されることはできません。 同じアトミック変数を解放する他のスレッドのデータ依存変数への書き込みは、現在のスレッド内で可視になります。 ほとんどのプラットフォームでは、これはコンパイラの最適化にのみ影響を与えます (後述の解放-消費順序付けを参照してください)。
memory_order_acquire このメモリ順序を持つロード操作は、影響を受けるメモリ位置に対して取得操作を行います。 現在のスレッド内の読み書きは、このロードの前に順序変更されることはできません。 同じアトミック変数を解放する他のスレッドのすべての書き込みは、現在のスレッド内で可視になります (後述の消費-取得順序付けを参照してください)。
memory_order_release このメモリ順序を持つストア操作は、解放操作を行います。 現在のスレッド内の読み書きは、このストアの後に順序変更されることはできません。 現在のスレッド内のすべての書き込みは、同じアトミック変数を取得する別のスレッド内で可視になり (後述の解放-取得順序付けを参照してください)、アトミック変数に依存性を伝播する書き込みは、同じアトミック変数を消費する他のスレッド内で可視になります (後述の解放-消費順序付けを参照してください)。
memory_order_acq_rel このメモリ順序を持つ読み込み-変更-書き込み操作は、取得操作解放操作の両方を行います。 現在のスレッド内のメモリの読み書きは、このストアの前や後に順序変更されることはできません。 同じアトミック変数を解放する他のスレッド内のすべての書き込みは、この変更の前に可視になり、この変更は、同じアトミック変数を取得する他のスレッド内で可視になります。
memory_order_seq_cst このメモリ順序を持つロード操作は解放操作を行い、ストア操作は取得操作を行い、読み込み-変更-書き込み操作は取得操作解放操作の両方を行い、さらにすべてのスレッドがすべての変更を同じ順序で観察する単一の全順序が存在します (後述の逐次一貫性順序付けを参照してください)。

[編集] 形式的な記述

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

[編集] 先行配列

評価順序で説明されているように、同じスレッド内において、評価 A は評価 B に対して先行配列される場合があります。

[編集] 依存性の伝播

同じスレッド内において、評価 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 に依存性を伝播する。

[編集] 変更順序

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

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

1) 書き込み-書き込み一貫性。 何らかのアトミックオブジェクト M を変更する評価 A (書き込み) が、 M を変更する評価 B に対して先行発生する場合、 M の変更順序において、 A は B より前に現れます。
2) 読み込み-読み込み一貫性。 何らかのアトミックオブジェクト M の値計算 A (読み込み) が、 M の値計算 B に対して先行発生し、 A の値が M に対する書き込み X から来た場合、 B の値は、 X によって格納された値か、 M の変更順序において X より後に現れる M に対する副作用 Y によって格納された値になります。
3) 読み込み-書き込み一貫性。 何らかのアトミックオブジェクト M の値計算 A (読み込み) が、 M の操作 B (書き込み) に対して先行発生する場合、 A の値は、 M の変更順序において B より前に現れる副作用 (書き込み) X から来ます。
4) 書き込み-読み込み一貫性。 アトミックオブジェクト M に対する副作用 (書き込み) X が、 M の値計算 (読み込み) B に対して先行発生する場合、評価 B は、その値を X か、 M の変更順序において X の後に現れる副作用 Y から取ります。

[編集] 解放シーケンス

アトミックオブジェクト M に対して解放操作 A が行われた後、以下のものから構成される M の変更順序の最も長い連続する部分シーケンスは、 A によって開始された解放シーケンスと言います。

1) A を行なったのと同じスレッドによって行われる書き込み。
2) 任意のスレッドによって M に行われたアトミックな読み込み-変更-書き込み操作。

[編集] 先行依存順序付け

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

1) A が何らかのアトミックオブジェクト M に対して解放操作を行い、異なるスレッドで同じアトミックオブジェクト M に対して B が消費操作を行い、 A によって開始された解放シーケンスの任意の一部によって書き込まれた値を B が読み込む。
2) A が X に対して先行依存順序付けされ、 X が B に依存性を伝播する。

[編集] スレッド間先行発生

スレッド間において、以下のいずれかが真の場合、評価 A は評価 B に対してスレッド間先行発生します。

1) A が B に対して同期する。
2) A が B に対して先行依存順序付けされる。
3) A が何らかの評価 X に対して同期し、 X が B に対して先行配列される。
3) A が何らかの評価 X に対して先行配列され、 X が B に対してスレッド間先行発生する。
4) A が何らかの評価 X に対してスレッド間先行発生し、 X が B に対してスレッド間先行発生する。

[編集] 先行発生

スレッドにかかわらず、以下のいずれかが真の場合、評価 A は評価 B に対して先行発生します。

1) A が B に対して先行配列される。
2) A が B に対してスレッド間先行発生する。

処理系は、必要に応じて追加の同期を導入することによって、先行発生関係が循環しないことを保証することが要求されます (これは消費操作が関わる場合にのみ必要になることがあります。 Batty et al を参照してください)。

ある評価があるメモリ位置を変更し、別の評価が同じメモリ位置を変更し、そのいずれかまたは両方がアトミック操作でない場合、その2つの評価の間に先行発生関係が存在しなければ、そのプログラムの動作は未定義です (プログラムにデータ競合があります)。

[編集] 可視な副作用

以下のどちらも真の場合、スカラーオブジェクト M に対する副作用 A (書き込み) は、 M の値計算 B (読み込み) に関して可視になります。

1) A が B に対して先行発生する。
2) A が X に対して先行発生し、 X が B に対して先行発生するような、 M に対する別の副作用 X が存在しない。

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

ノート: スレッド間の同期は、 (先行発生関係を確立することによる) データ競合の回避と、どのような状況でどの副作用が可視になるかの定義によって、表されます。

[編集] 消費操作

memory_order_consume またはそれ以上の強さのメモリ順序を持つアトミックロードは、消費操作です。 std::atomic_thread_fence は消費操作より強い同期要件を課すことに注意してください。

[編集] 取得操作

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

[編集] 解放操作

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

[編集] 説明

[編集] 緩和された順序付け

memory_order_relaxed が指定されたアトミック操作は、同期操作ではありません。 これは並行的なメモリアクセス間に順序制約を課しません。 アトミック性と変更順序の一貫性のみが保証されます。

例えば、初期値がゼロの xy を用いて、以下のコードを考えてみます。

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

このコードは、 r1 == r2 == 42 を生成する可能性があります。 スレッド 1 では A が B に対して先行配列され、スレッド 2 では C が D に対して先行配列されますが、 y の変更順序において D が A より前に現れることを阻むものはなく、 x の変更順序において B が C より前に現れることを阻むものもないためです。 y に対する D の副作用がスレッド 1 のロード A に可視になるのと同時に、 x に対する B の副作用がスレッド 2 のロード C に可視になる可能性があります。 特に、コンパイラや実行時の順序変更のためにスレッド 2 において C より前に D が完了すれば、これが発生する可能性があります。

緩和されたメモリモデルであっても、自身の計算の循環依存で虚空から値が発生することは許されていません。 例えば、初期値がゼロの xy を用いて、以下のコードを考えてみます。

// スレッド 1:
r1 = x.load(memory_order_relaxed);
if (r1 == 42) y.store(r1, memory_order_relaxed);
// スレッド 2:
r2 = y.load(memory_order_relaxed);
if (r2 == 42) x.store(42, memory_order_relaxed);

このコードが r1 == r2 == 42 を生成することは許されていません。 y に 42 が格納されることは、 x に 42 が格納される場合にのみ起き得るのであり、そしてそれは y に 42 が格納されることに循環依存しています。 C++14 までは、これは仕様上は論理的には許されていましたが、処理系に対して推奨されてはいませんでした。

(C++14およびそれ以降)

緩和されたメモリ順序の一般的な用途は、std::shared_ptr の参照カウンタのような、カウンタのインクリメントです。 これはアトミック性のみが要求され、順序付けや同期は不要なためです (ただし、 shared_ptr カウンタのデクリメントでは、デストラクタとの間で取得-解放同期が要求されることに注意してください)。

#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
 
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

[編集] 解放-取得順序付け

スレッド A のアトミックストアに memory_order_release が指定されており、スレッド B の同じ変数からのアトミックロードに memory_order_acquire が指定されている場合、スレッド A の視点でそのアトミックストアに対して先行発生するすべてのメモリ書き込み (非アトミックなものおよび緩和されたアトミックなもの) は、スレッド B で可視な副作用になります。 つまり、いったんそのアトミックロードが完了すれば、スレッド B は、スレッド A がメモリに書き込んだすべてのものを見ることが保証されます。

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

強い順序付けを持つシステム (x86、SPARC TSO、IBM のメインフレーム) では、解放-取得順序付けは、多くの操作に対して自動的に適用されます。 この同期モードに対して追加の CPU 命令は発行されません。 特定のコンパイラの最適化に影響するだけです (例えば、コンパイラは非アトミックストアを解放アトミックストアの後に移動したり、非アトミックロードを取得アトミックロードより前に行ったりすることはできません)。 弱い順序付けを持つシステム (ARM、Itanium、PowerPC) では、 CPU の特別なロード命令やメモリフェンス命令が使用されます。

相互排他ロック (std::mutexアトミックなスピンロックなど) は、解放-取得同期の例です。 ロックがスレッド A によって解放され、スレッド B によって取得されるとき、スレッド A のコンテキストにおいてクリティカルセクション内で (ロック解放前に) 行われたすべてのことは、同じクリティカルセクションを実行している (ロック取得後の) スレッド B に対して、可視にならなければなりません。

#include <thread>
#include <atomic>
#include <cassert>
#include <string>
 
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つのスレッド間で推移的な解放-取得順序付けをデモンストレーションします。

#include <thread>
#include <atomic>
#include <cassert>
#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;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
        expected = 1;
    }
}
 
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    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();
}


[編集] 解放-消費順序付け

スレッド A のアトミックストアに memory_order_release が指定されており、スレッド B の同じ変数からのアトミックロードに memory_order_consume が指定されている場合、スレッド A の視点でそのアトミックストアに対して先行依存順序付けされるすべてのメモリ書き込み (非アトミックなものおよび緩和されたアトミックなもの) は、スレッド B でそのロード操作が依存性を伝播する操作内で可視な副作用になります。 つまり、いったんそのアトミックロードが完了すれば、そのロードから取得した値を使用するスレッド B の操作や関数は、スレッド A がメモリに書き込んだものを見ることが保証されます。

同期は、同じアトミック変数を解放および消費するスレッド間でのみ確立されます。 他のスレッドは、同期しているスレッドのいずれか、あるいはいずれとも異なる順序でメモリアクセスを見る可能性があります。

DEC Alpha を除くすべての主流な CPU では、依存順序付けは自動的に適用されます。 この同期モードに対して追加の CPU 命令は発行されません。 特定のコンパイラの最適化に影響するだけです (例えば、コンパイラは依存連鎖に影響するオブジェクトを投機的にロードすることはできません)。

この順序付けの一般的なユースケースは、滅多に書き込まれない並行データ構造 (ルーティングテーブル、コンフィグレーション、セキュリティポリシー、ファイアウォールのルールなど) への読み込みアクセスや、ポインタを介した公開を行う出版-購読のパターン、つまり、生産者がポインタを公開し、消費者がそのポインタを通して情報にアクセスする場合です。 消費者に可視なメモリに生産者が書き込む以外のこと (弱い順序付けのアーキテクチャでは高価な操作となる場合があります) は、する必要がありません。 そのようなシナリオの例は rcu_dereference です。

依存連鎖の細かい制御については、 std::kill_dependency および [[carries_dependency]] も参照してください。

現在 (2015年2月)、依存連鎖を追跡する既知の製品コンパイラはありません。 消費操作は取得操作に引き上げられます。

解放-消費順序付けの仕様は改訂中です。 memory_order_consume の使用は一時的に非推奨になっています。

(C++17およびそれ以降)

この例はポインタを介した公開のために依存順序付けされた同期をデモンストレーションします。 整数データは、文字列へのポインタに対してデータ依存関係を持たないため、消費者において、その値は未定義です。

#include <thread>
#include <atomic>
#include <cassert>
#include <string>
 
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 が指定されたアトミック操作は、解放-取得順序付けと同じ方法でメモリを順序付けする (あるスレッドのストアに対して先行発生するすべてのことが、ロードを行ったスレッドで可視な副作用となる) だけでなく、それが指定されたすべてのアトミック操作の単一の変更の全順序も確立します。

形式的には、

アトミック変数 M からロードする各々の memory_order_seq_cst な操作 B は、以下のいずれかを観察します。

  • 単一の全順序において B より前に現れる、 M を変更する最後の操作 A の結果。
  • または、そのような A が存在した場合、 B は、 memory_order_seq_cst でなく、 A に対して先行発生しない、何らかの M の変更の結果を観察するかもしれません。
  • または、そのような A が存在しなかった場合、 B は、 memory_order_seq_cst でない、何らかの無関係な M の変更の結果を観察するかもしれません。

B に対して先行配列される memory_order_seq_cststd::atomic_thread_fence 操作 X が存在した場合、 B は以下のいずれかを観察します。

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

M の値を書き込むアトミック操作 A および M の値を読み込むアトミック操作 B について、2つの memory_order_seq_cststd::atomic_thread_fence 操作 X および Y が存在し、 A が X に対して先行配列され、 Y が B に対して先行配列され、単一の全順序において X が Y より前に現れる場合、 B は以下のいずれかを観察します。

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

M のアトミックな変更操作 A および B について、以下の場合、 M の変更順序において、 B が A より後に発生します。

  • A が X に対して先行配列され、単一の全順序において X が B より前に現れるような、 memory_order_seq_cststd::atomic_thread_fence 操作 X が存在する。
  • または、 Y が B に対して先行配列され、単一の全順序において A が Y より前に現れるような、 memory_order_seq_cststd::atomic_thread_fence 操作 Y が存在する。
  • または、 A が X に対して先行配列され、 Y が B に対して先行配列され、単一の全順序において X が Y より前に現れるような、 memory_order_seq_cststd::atomic_thread_fence 操作 X および Y が存在する。

これは以下の事柄を意味することに注意してください。

1) memory_order_seq_cst でないアトミック操作が登場した途端、逐次一貫性は失われます。
2) 逐次一貫性のフェンスは、一般的なケースのアトミック操作に対してではなく、フェンス自身に対して全順序を確立するだけです (先行発生と異なり、先行配列はスレッド間の関係ではありません)。

複数の生産者と複数の消費者がいる状況で、すべての生産者が起こしたアクションを同じ順序ですべての消費者が観察しなければならない場合、逐次一貫性が必要になるかもしれません。

すべてのマルチコアシステムにおいて、逐次的な全順序は CPU のフルメモリフェンス命令を要求します。 これは影響のあるメモリアクセスをすべてのコアに伝搬することを強制するため、性能のボトルネックになる場合があります。

この例は、逐次一貫性順序付けが必要な状況をデモンストレーションします。 スレッド cd が、アトミック xy に対する変更を逆の順序で観察する可能性があるため、他のいかなる順序付けも assert に引っ掛かる可能性があります。

#include <thread>
#include <atomic>
#include <cassert>
 
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 glvalue を通したアクセス (読み込みおよび書き込み) は、同じスレッド内の先行配列または後続配列関係を持つ観察可能な副作用 (他の volatile アクセスも含む) を超えて順序変更されることはできませんが、 volatile アクセスはスレッド間の同期を確立しないため、この順序が他のスレッドから観察できることは保証されません。

さらに、 volatile アクセスはアトミックでなく (並行的な読み書きはデータ競合になります)、メモリを順序付けません (非 volatile メモリアクセスは volatile アクセスの周りを自由に順序変更される可能性があります)。

ひとつの重要な例外は Visual Studio です。 Visual Studio では、デフォルトの設定では、すべての volatile 書き込みは解放操作であり、すべての volatile 読み込みは取得操作であり (MSDN)、そのため volatile をスレッド間の同期に使用することができます。 標準の volatile の意味論は、例えば sig_atomic_t 変数に適用すれば、同じスレッドで実行する std::signal ハンドラと通信するためには十分ですが、マルチスレッドプログラミングに適用することはできません。

[編集] 関連項目

memory orderC言語リファレンス

[編集] 外部リンク