web-dev-qa-db-ja.com

並行性:C ++ 11メモリモデルのアトミックおよび揮発性

グローバル変数は、2つの異なるコアで同時に実行されている2つのスレッド間で共有されます。スレッドは変数に書き込み、変数から読み取ります。アトミック変数の場合、1つのスレッドが古い値を読み取ることができますか?各コアのキャッシュにはシェア変数の値があり、1つのスレッドがキャッシュ内のコピーに書き込むと、別のコア上の他のスレッドが独自のキャッシュから古い値を読み取る場合があります。または、コンパイラは他のキャッシュから最新の値を読み取るために強力なメモリ順序付けを行いますか? c ++ 11標準ライブラリはstd :: atomicをサポートしています。 volatileキーワードとの違いは何ですか?上記のシナリオでどのように揮発性およびアトミックタイプが異なる動作をしますか?

53
Abhijit_K

まず、volatileはアトミックアクセスを意味しません。メモリマップドI/Oや信号処理などのために設計されています。 volatileは、_std::atomic_と一緒に使用する場合は完全に不要です。プラットフォームのドキュメントがない限り、volatileは、スレッド間のアトミックアクセスまたはメモリの順序付けには影響しません。

次のようなスレッド間で共有されるグローバル変数がある場合:

_std::atomic<int> ai;
_

可視性と順序の制約は、操作に使用するメモリ順序パラメーター、およびロック、スレッド、他のアトミック変数へのアクセスの同期効果に依存します。

追加の同期がない場合、1つのスレッドがaiに値を書き込むと、別のスレッドが特定の期間に値を参照することを保証するものは何もありません。標準では、「妥当な期間内」に表示されるように指定されていますが、アクセスによって古い値が返される場合があります。

_std::memory_order_seq_cst_のデフォルトのメモリ順序は、すべての変数にわたるすべての_std::memory_order_seq_cst_操作の単一のグローバルな合計順序を提供します。これは、古い値を取得できないことを意味するものではありませんが、取得する値が、操作がこの全体的な順序のどこにあるかによって決定および決定されることを意味します。

2つの共有変数xおよびyがあり、最初はゼロで、1つのスレッドがxに1を書き込み、別のスレッドがyに2を書き込み、次に3番目のスレッドが操作の間に順序の制約がないため、両方とも(0,0)、(1,0)、(0,2)、または(1,2)のいずれかを参照できます。したがって、操作はグローバルな順序で任意の順序で表示されます。 。

両方の書き込みが_x=1_の前に_y=2_を実行し、読み取りスレッドがyの前にxを読み取る同じスレッドからのものである場合、(0,2)は有効なオプションではなくなります、_y==2_の読み取りは、xへの以前の書き込みが可視であることを意味するためです。 2つの読み取りが2つの書き込みとインターリーブする方法に応じて、他の3つのペア(0,0)、(1,0)、および(1,2)が引き続き可能です。

_std::memory_order_relaxed_や_std::memory_order_acquire_などの他のメモリ順序を使用する場合、制約はさらに緩和され、単一のグローバル順序は適用されなくなります。追加の同期がない場合、スレッドは、変数を分離するための2つのストアの順序について必ずしも同意する必要さえありません。

「最新」の値を保証する唯一の方法は、exchange()compare_exchange_strong()、またはfetch_add()などの読み取り-変更-書き込み操作を使用することです。読み取り-変更-書き込み操作には、常に「最新の」値を操作するという追加の制約があります。そのため、一連のスレッドによる一連のai.fetch_add(1)操作は、重複やギャップのない値のシーケンスを返します。追加の制約がない場合でも、どのスレッドがどの値を見るかはまだ保証されていません。

アトミック操作の操作は複雑なトピックです。多くの背景資料を読み、公開コードを調べてから、アトミックで実動コードを作成することをお勧めします。ほとんどの場合、ロックを使用するコードを書く方が簡単で、それほど効率的ではありません。

84

volatileとアトミック操作は異なる背景を持ち、異なる意図で導入されました。

volatileはさかのぼって作成されたもので、主にメモリマップIOにアクセスする際のコンパイラの最適化を防ぐように設計されています。最近のコンパイラーはvolatileの最適化を抑制する以上のことはしない傾向がありますが、一部のマシンでは、これはメモリーにマップされたIOでも十分ではありません。シグナルハンドラーの特別な場合、およびsetjmplongjmpおよびgetjmpシーケンスを除く(C標準、およびシグナルの場合、Posix標準では、保証)、特別な追加命令(フェンスまたはメモリバリア)がないと、ハードウェアが特定のアクセスを並べ替えたり、さらには抑制したりする現代のマシンでは役に立たないと見なされる必要があります。 setjmpなどを使用するべきではないのでC++では、これは多かれ少なかれシグナルハンドラを残します。マルチスレッド環境では、少なくともUnixでは、それらに対してもより良いソリューションがあります。また、カーネルコードで作業しており、コンパイラが問題のプラットフォームに必要なものを生成することを保証できる場合は、メモリマップIOも可能です。 (標準によれば、volatileアクセスは観察可能な動作であり、コンパイラはそれを尊重する必要があります。しかし、コンパイラは「アクセス」の意味を定義し、ほとんどの場合「ロードまたはストアマシン」として定義しますこれは、最新のプロセッサでは、バス上に必ずしも読み取りまたは書き込みサイクルが存在することを意味するものではなく、期待した順序ではありません。)

この状況を考慮して、C++標準はアトミックアクセスを追加しました。これにより、スレッド間で一定数の保証が提供されます。特に、アトミックアクセスを中心に生成されるコードには、ハードウェアがアクセスを並べ替えるのを防ぎ、アクセスがマルチコアマシン上のコア間で共有されるグローバルメモリまで伝播するために必要な追加命令が含まれます。 (標準化の取り組みのある時点で、Microsoftはこれらのセマンティクスをvolatileに追加することを提案しましたが、C++コンパイラの一部はそれを行うと思います。ただし、委員会の問題について議論した後、Microsoft代表— volatileを元の意味のままにして、アトミック型を定義する方が良いということでした。)または、コードで必要な命令を実行するミューテックスなどのシステムレベルプリミティブを使用するだけです。 (それらは必要です。メモリアクセスの順序に関するいくつかの保証なしにミューテックスを実装することはできません。)

30
James Kanze

揮発性とアトミックは異なる目的を果たします。

揮発性:最適化を回避するようコンパイラーに通知します。このキーワードは、予想外に変化する変数に使用されます。そのため、ハードウェアステータスレジスタ、ISRの変数、マルチスレッドアプリケーションで共有される変数を表すために使用できます。

Atomic:マルチスレッドアプリケーションの場合にも使用されます。ただし、これにより、マルチスレッドアプリケーションでの使用中にロック/ストールが発生しないことが保証されます。原子操作は人種がなく、分割不可能です。使用の主要なシナリオのいくつかは、ロックが無料か使用されているかを確認し、値にアトミックに追加し、マルチスレッドアプリケーションで追加された値を返すなどです。

3

2つのことの基本的な概要を次に示します。

1)揮発性キーワード:
この値はいつでも変更される可能性があるため、レジスターにキャッシュしないでくださいとコンパイラーに伝えます。 Cで古い「register」キーワードを検索します。「Volatile」は、基本的に「register」の「+」に対する「-」演算子です。最近のコンパイラは、「レジスタ」がデフォルトで明示的に要求するために使用する最適化を行うようになったため、「揮発性」のみが表示されます。 volatile修飾子を使用すると、処理で古い値が使用されることはなく、それ以上は使用されないことが保証されます。

2)アトミック:
アトミック操作は単一のクロック刻みでデータを変更するため、他のスレッドがそのような更新の途中でデータにアクセスすることは不可能です。通常、ハードウェアがサポートする単一クロックのアセンブリ命令に限定されます。 ++、-、2つのポインターの交換など。これは、異なるスレッドがアトミック命令を実行するORDERについては何も言っていないことに注意してください。そのため、注文を強制するための追加オプションがすべてあります。

3
Zack Yezek