memory_order_consume

std::memory_order_consume は、6 つのメモリ モデルの中で最も伝説的なものです。それには 2 つの理由があります。一方では、std::memory_order_consume を取得するのは非常に困難です。一方、これは将来変更される可能性がありますが、これをサポートするコンパイラはありません。

コンパイラが C++11 標準をサポートしているのに、メモリ モデル std::memory_order_consume をサポートしていないというのは、どうしてでしょうか?答えは、コンパイラが std::memory_order_consume を std::memory_order_acquire にマップすることです。どちらもロード操作または取得操作であるため、これで問題ありません。 std::memory_order_consume には、より弱い同期と順序付けの制約が必要です。したがって、リリースと取得の順序付けは、リリースと消費の順序付けよりも遅くなる可能性がありますが、これが重要な点であり、明確に定義されています。

release-consume の順序を理解するには、release-acquire の順序と比較することをお勧めします。この投稿では、std::memory_order_consume と std::memory_order_acquire の強い関係を強調するために、取得-解放セマンティックからではなく、解放-取得順序から明示的に話します。

リリースと取得の順序付け

開始点として、2 つのスレッド t1 と t2 を持つプログラムを使用します。 t1 は生産者の役割を果たし、t2 は消費者の役割を果たします。アトミック変数 ptr は、プロデューサーとコンシューマーの同期に役立ちます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// acquireRelease.cpp

#include <atomic>
#include <thread>
#include <iostream>
#include <string>
 
std::atomic<std::string*> ptr;
int data;
std::atomic<int> atoData;
 
void producer(){
 std::string* p = new std::string("C++11");
 data = 2011;
 atoData.store(2014,std::memory_order_relaxed);
 ptr.store(p, std::memory_order_release);
}
 
void consumer(){
 std::string* p2;
 while (!(p2 = ptr.load(std::memory_order_acquire)));
 std::cout << "*p2: " << *p2 << std::endl;
 std::cout << "data: " << data << std::endl;
 std::cout << "atoData: " << atoData.load(std::memory_order_relaxed) << std::endl;
}
 
int main(){
 
 std::cout << std::endl;
 
 std::thread t1(producer);
 std::thread t2(consumer);
 
 t1.join();
 t2.join();
 
 std::cout << std::endl;
 
}

プログラムを分析する前に、小さなバリエーションを導入したいと思います。 21 行目のメモリ モデル std::memory_order_acquire を std::memory_order_consume に置き換えます。

リリース消費の注文

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// acquireConsume.cpp

#include <atomic>
#include <thread>
#include <iostream>
#include <string>
 
std::atomic<std::string*> ptr;
int data;
std::atomic<int> atoData;
 
void producer(){
 std::string* p = new std::string("C++11");
 data = 2011;
 atoData.store(2014,std::memory_order_relaxed);
 ptr.store(p, std::memory_order_release);
}
 
void consumer(){
 std::string* p2;
 while (!(p2 = ptr.load(std::memory_order_consume)));
 std::cout << "*p2: " << *p2 << std::endl;
 std::cout << "data: " << data << std::endl;
 std::cout << "atoData: " << atoData.load(std::memory_order_relaxed) << std::endl;
}
 
int main(){
 
 std::cout << std::endl;
 
 std::thread t1(producer);
 std::thread t2(consumer);
 
 t1.join();
 t2.join();
 
 std::cout << std::endl;
 
}

それは簡単でした。しかし今、プログラムは未定義の動作をしています。私のコンパイラは std::memory_order_acquire によって std::memory_order_consume を実装しているため、このステートメントは非常に仮説的です。したがって、内部では、両方のプログラムが実際に同じことを行います。

リリース取得とリリース消費の順序付け

プログラムの出力は同一です。

繰り返しになりますが、最初のプログラム acquireRelease.cpp が明確に定義されている理由を簡単に説明したいと思います。

16 行目の store 操作は、21 行目の load 操作と同期しています。その理由は、store 操作が std::memory_order_release を使用し、load 操作が std::memory_order_acquire を使用しているためです。それが同期でした。 release-acquire 順序付けの順序制約についてはどうですか? release-acquire 順序付けは、store 操作 (16 行目) の前のすべての操作が load 操作 (21 行目) の後で使用できることを保証します。そのため、解放/取得操作は、非アトミック変数データ (14 行目) とアトミック変数 atoData (15 行目) へのアクセスに加えて命令します。これは、atoData が std::memory_order_relaxed メモリ モデルを使用しているにもかかわらず保持されます。

重要な質問は.プログラム std::memory_order_acquire を std::memory_order_consume に置き換えるとどうなりますか?

std::memory_order_consume によるデータの依存関係

std::memory_order_consume は、アトミックに対するデータの依存関係に関するものです。データの依存関係は 2 つの方法で存在します。最初は carries-a-dependency-to スレッド内および dependency-ordered_before 2 つのスレッド間。両方の依存関係で happens-before が導入されます 関係。これは、明確に定義されたプログラムが必要とするこの種の関係です。しかし、carries-a-dependency-to とはどういう意味ですか? および dependency-order-before ?

  • Carry-a-dependency-to: 操作 A の結果が操作 B のオペランドとして使用される場合、次のようになります。 B.
  • 前の依存関係: ストア操作 (std::memory_order_release、std::memory_order_acq_rel、または std::memory_order_seq_cst を使用) は dependency-ordered-before です ロード操作 B (std::memory_order_consume を使用)、ロード操作 B の結果が同じスレッド内のさらなる操作 C で使用される場合。操作 B と C は同じスレッドにある必要があります。

もちろん、私は個人的な経験から、両方の定義を消化するのは容易ではないことを知っています.そこで、視覚的に説明するためにグラフィックを使用します.

式 ptr.store(p, std::memory_order_release) は dependency-ordered-before です while (!(p2 =ptr.load(std::memory_order_consume)))、次の行で std::cout <<"*p2:" <<*p2 <carries-a-dependency-to std::cout <<"*p2:" <<*p2 <

ただし、以下の data および atoData の出力については保証しません。これは、どちらにもキャリー ア ディペンデンシーがないためです。 ptr.load 操作に関連します。しかし、それはさらに悪化します。データは非アトミック変数であるため、データには競合状態があります。その理由は、両方のスレッドが同時にデータにアクセスでき、スレッド t1 がデータを変更したいからです。したがって、プログラムは未定義です。

次は?

それが挑戦的な投稿だったことを認めます。次の投稿では、取得と解放のセマンティックに関する典型的な誤解について説明します。これは、取得操作が解放操作の前に実行された場合に発生します。