サーチ…


備考

少なくとも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_releasestd::memory_order_acquire load-acquirestore-releaseによって格納された値を確認した後、 store-release発生する前にシーケンスが store-releaseされてからload-acquireされた後にロードが順序付けされる
std::memory_order_consume memory_order_acquireようなものmemory_order_acquireが、依存負荷の場合のみです
std::memory_order_acq_rel load-acquirestore-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-acquirestore-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==truexまたは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==2y==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フラグがセットされるまで回転します。次に、単一の獲得フェンスを使用して、必要なメモリ順序を提供する。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow