9

私は次のC++2011コードを持っています:

std::atomic<bool> x, y;
std::atomic<int> z;

void f() {
   x.store(true, std::memory_order_relaxed);
   std::atomic_thread_fence(std::memory_order_release);
   y.store(true, std::memory_order_relaxed);
}

void g() {
   while (!y.load(std::memory_order_relaxed)) {}
   std::atomic_thread_fence(std::memory_order_acquire);
   if (x.load(std::memory_order_relaxed)) ++z;
}

int main() {
   x = false;
   y = false;
   z = 0;
   std::thread t1(f);
   std::thread t2(g);
   t1.join();
   t2.join();
   assert(z.load() !=0);
   return 0;
}

私のコンピュータアーキテクチャクラスでは、このコードのアサートは常に真になると言われています。しかし、今それを徹底的に検討した後、私はそれがなぜそうなのか本当に理解できません。

私が知っていることについて:

  • ' memory_order_release 'のあるフェンスは、それ以降に前のストアを実行することを許可しません
  • ' memory_order_acquire 'のあるフェンスでは、その後のロードをその前に実行することはできません。

私の理解が正しければ、なぜ次の一連のアクションが発生しないのですか?

  1. t1の内部y.store(true, std::memory_order_relaxed);はと呼ばれます
  2. t2は完全に実行され、「x」をロードすると「false」が表示されるため、ユニット内のzは増加しません。
  3. t1は実行を終了します
  4. メインスレッドでは、z.load()が0を返すため、アサートは失敗します

これは「取得」-「解放」ルールに準拠していると思いますが、たとえば、この質問のベストアンサー:私の場合と非常によく似たc ++ 11メモリフェンスを理解すると、私の場合のステップ1のようなものが示唆されます一連のアクションは「memory_order_release」の前に発生することはできませんが、その背後にある理由から詳細には触れません。

私はこれについてひどく戸惑っています、そして誰かがそれに光を当てることができれば非常にうれしいです:)

4

2 に答える 2

4

これらの各ケースで正確に何が起こるかは、実際に使用しているプロセッサによって異なります。たとえば、x86はキャッシュコヒーレントアーキテクチャであるため、おそらくこれを主張しません(レース条件を設定できますが、プロセッサからキャッシュ/メモリに値が書き出されると、他のすべてのプロセッサがその値を読み取ります-もちろん、別のプロセッサが直後に別の値を書き込むのを停止することはありません。

したがって、これがARMまたはそれ自体でキャッシュコヒーレントであることが保証されていない同様のプロセッサで実行されていると仮定すると、次のようになります。

への書き込みxはの前に行われるためmemory_order_release、t2ループはwhile(y...)untilxもtrueになるまで終了しません。これは、x後で読み取られるときに1つであることが保証されているため、z更新されることを意味します。release私の唯一のわずかな質問は、 forも必要ないかどうかですz...ととmainは異なるプロセッサで実行されている場合は、 。に古い値が含まれている可能性があります。t1t2zmain

もちろん、マルチタスクOSを使用している場合(または十分な処理を実行する割り込みなど)、これが発生することは保証されていません。t1を実行したプロセッサがキャッシュをフラッシュすると、t2がxの新しい値を読み取る可能性があるためです。

そして、私が言ったように、これはx86プロセッサ(AMDまたはIntelのもの)にはその影響を与えません。

したがって、一般的なバリア命令を説明するには(IntelおよびAMD process0rsにも適用可能):

まず、命令は順不同で開始および終了する可能性がありますが、プロセッサには一般的な順序の「理解」があることを理解する必要があります。この「疑似マシンコード」があるとしましょう。

 ...
 mov $5, x
 cmp a, b
 jnz L1
 mov $4, x

L1:..。

mov $4, xプロセッサは、「jnz L1」を完了する前に投機的に実行される可能性があります。したがって、この事実を解決するには、プロセッサが取得されmov $4, xた場合にをロールバックする必要がありjnz L1ます。

同様に、次の場合:

 mov $1, x
 wmb         // "write memory barrier"
 mov $1, y

プロセッサには、「wmbの後に発行されたストア命令は、完了する前にすべてのストアが実行されるまで実行しないでください」というルールがあります。これは「特別な」命令です。メモリの順序を保証するという正確な目的のためにあります。それが行われていない場合は、プロセッサが壊れており、設計部門の誰かが「彼のお尻をライン上に」持っています。

同様に、「読み取りメモリバリア」は、プロセッサの設計者が、バリア命令の前に保留中の読み取りを完了するまで、プロセッサが別の読み取りを完了しないことを保証する命令です。

「実験的な」プロセッサや正しく動作しないいくつかの卑劣なチップに取り組んでいない限り、それはそのように動作します。それはその命令の定義の一部です。このような保証がなければ、(安全な)スピンロック、セマフォ、ミューテックスなどを実装することは不可能です(または少なくとも非常に複雑で「高価」です)。

多くの場合、「暗黙のメモリバリア」、つまり、メモリバリアがない場合でもメモリバリアを引き起こす命令があります。ソフトウェア割り込み(「INTX」命令など)はこれを行う傾向があります。

于 2013-01-24T01:23:22.873 に答える
3

「このプロセッサがこれを行う、そのプロセッサがそれを行う」という観点から、C++の同時実行性の質問について議論するのは好きではありません。C ++ 11にはメモリモデルがあり、このメモリモデルを使用して、何が有効で何が無効かを判断する必要があります。CPUアーキテクチャとメモリモデルは通常、理解するのがさらに困難です。さらに、それらは複数あります。

これを念頭に置いて、次のことを考慮してください。スレッドt2は、t1がy.storeを実行し、変更がt2に伝播されるまで、whileループでブロックされます。(ちなみに、これは理論的には不可能です。しかし、それは現実的ではありません。)したがって、t1のy.storeとt2のy.loadの間には、ループを離れることができる発生前の関係があります。

さらに、x.storeとリリースバリア、およびバリアとy.storeの間の関係の前に、単純なスレッド内が発生します。

t2では、真に戻る負荷と取得バリアおよびx.loadの間に発生前があります。

発生する前は推移的であるため、リリースバリアは取得バリアの前に発生し、x.storeはx.loadの前に発生します。障壁があるため、x.storeはx.loadと同期します。これは、ロードが格納されている値を確認する必要があることを意味します。

最後に、z.add_and_fetch(ポストインクリメント)は、スレッドの終了前に発生します。これは、メインスレッドがt2.joinからウェイクアップする前に発生します。これは、メインスレッドのz.loadの前に発生するため、zを変更する必要があります。メインスレッドに表示されます。

于 2013-01-24T12:35:04.110 に答える