最終更新日時:
が更新

履歴 編集

enum
<atomic>

std::memory_order(C++11)

namespace std {
  // C++11
  enum memory_order {
    memory_order_relaxed, memory_order_consume, memory_order_acquire,
    memory_order_release, memory_order_acq_rel, memory_order_seq_cst
  };

  // 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++26
  enum class memory_order : unspecified {
    relaxed = 0,              acquire = 2,
    release = 3, acq_rel = 4, seq_cst = 5
  };
  inline constexpr memory_order memory_order_relaxed = memory_order::relaxed;
  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;
}

概要

コンパイラに許可されている最適化の一つに、「プログラムの意味を変えない限りにおいて、メモリアクセスの順番を変えたり、省略したりしてもよい」というものがある。また、マルチコアCPUにおいては、あるCPUコアによるメモリアクセスの順序が他のコアからも同じように見えるとは限らない。このような挙動はマルチスレッドプログラミングにおいて問題になることがある。 この問題への対処として、C++11では各スレッドの実行に順序付けをするための「よりも前に発生する」という関係を定義し、それによってあるスレッドでの操作が他スレッドから可視になるか否かを定めている。 atomic変数においては、"release"操作によって書き込まれた値を"acquire"操作によって別のスレッドが読み出した場合に、そのrelease操作とacquire操作の間に順序付けが行われる。以下に例を挙げる。

#include <iostream>
#include <atomic>
#include <thread>
int data;
std::atomic<bool> ready(false);

void f()
{
  while (!ready.load(std::memory_order_acquire)) {
  }

  std::cout << data << std::endl;   // (2)
}

int main()
{
  std::thread t(f);

  data = 3;   // (1)
  ready.store(true, std::memory_order_release);

  t.join();
}

出力

3

atomic<bool>型の変数readyへの読み書きに注目すると、main()では変数readytrue を"release"操作として書き込み、f()では"acquire"操作としての読み込みを true が返されるまで繰り返している。よって、f()whileループを抜けた時点で、main()ready.store()f()ready.load()の間に順序付け(「よりも前に発生する」関係)が成立している。 ここでさらに変数dataへの読み書き(1), (2)に注目すると、(1)はready.store()より前、(2)はready.load()より後にあるので、以下のようなスレッド間の順序付け(「よりも前に発生する」関係)が成立することになる。 (1) → ready.store()ready.load() → (2) よって、(1)における書き込みが(2)の時点で可視であることが保証される。 このようにしてC++のマルチスレッドプログラムにおける実行順序および可視性を理解することができる。

以下の列挙値はatomic変数への操作に指定可能な順序付けの種類を表す。

列挙値 説明
relaxed スレッド間の順序付けの効果は一切持たない。
consume C++26で非推奨
acquire acquire操作としての読み込みを行うことを指示する。store()など、書き込みのみを行う操作に対しては指定できない。
release release操作としての書き込みを行うことを指示する。load()など、読み込みのみを行う操作に対しては指定できない。
acq_rel 読み込みと書き込みを同時に行う操作(Read-Modify-Write操作)に対してのみ指定することができ、acquireとreleaseを合わせた効果を持つ。
seq_cst acquire(読み込み操作の場合)、release(書き込み操作の場合)、acq_rel(Read-Modify-Write操作の場合)としての効果を持つ。さらに、同じseq_cstが指定されている他のatomic操作との間での順序一貫性も保証する。これは最も強い保証であり、標準のatomic操作におけるデフォルトのメモリオーダーとして使用される。「seq_cst」は「sequential consistency(順序一貫性)」を意味する。

備考

Read-Modify-Write操作が読み取る値

読み込みと書き込みを同時に行う操作 (Read-Modify-Write操作。fetch_add()compare_exchange_weak()など) は、指定したメモリオーダーによらず、対象のアトミックオブジェクトの変更順序において、自身の副作用の直前にある副作用が書き込んだ値を読み取る。

これにより、複数のスレッドが同じアトミックオブジェクトに対してRead-Modify-Write操作を行っても、それらが同じ値を読み取ることはない。

consume列挙値

非推奨化されたconsume列挙値の挙動はacquire操作と似ているが、それより弱い順序付けでの読み込みを行うことを指示する。acquire操作は後続の全ての操作に対して順序付けを行うのに対し、consume操作は読み込まれた値に依存(ただし条件分岐による依存は除く)する操作のみに順序付けを保証する点が異なる。 複雑なconsume操作を正しく実装するC++コンパイラは登場せず、より単純なacquire操作として扱われていた。C++20では仕様再検討に伴う一時的な利用回避が宣言され、最終的には役に立たないとの判断からC++26で非推奨となった。

relaxed操作とout-of-thin-air(無から生じる)値

relaxedは順序付けの効果を一切持たないが、それでも「out-of-thin-air値(無から生じる値)」は計算されるべきではない、と規格は推奨している。out-of-thin-air値とは、複数スレッドのアトミック操作が互いの結果に循環依存することで、プログラムのどこにも書かれていない値が理屈の上では「無から湧いて」しまう現象である。

// x, yはatomic<int>で、初期値0とする

// スレッド1:
r1 = y.load(std::memory_order::relaxed);
x.store(r1, std::memory_order::relaxed);

// スレッド2:
r2 = x.load(std::memory_order::relaxed);
y.store(r2, std::memory_order::relaxed);

このプログラムでは、relaxedの順序付けの規則だけからはr1 == r2 == 42のような実行を排除できない。yへの42の書き込みはxが42を格納する場合にのみ起こり、それはyが42を格納することに循環依存している。このような値の計算を実装は行うべきではない、という推奨である。

C++29では、この推奨に加えて、通常の実装ではout-of-thin-air値がそもそも生じないことの根拠が注記として追加された。

  • 非volatileのアトミックアクセスをvolatile相当として扱う(ソースコードに書かれたとおりマシン命令へ変換する)実装は、未定義動作のないプログラムに対してout-of-thin-air値を生成しない
  • スレッド単位の解析のみで最適化を行う実装、すなわち同一オブジェクトへの非volatileアトミックアクセスの省略・マージや、異なるオブジェクトへのアトミックアクセスの並べ替えは(as-ifルールが許す範囲で)行うが、アトミックアクセスの発明はしない実装も、同様にout-of-thin-air値を生成しない
  • 物理マシンは投機実行を注意深く管理しており、ハードウェアのレベルでもout-of-thin-air値は生成されない

現実のコンパイラとハードウェアはこれらの条件を満たしているため、実装がout-of-thin-air値の回避のために特別な対策をとる必要はなく、ユーザーコードの変更も不要である。

バージョン

言語

  • C++11

処理系

参照