しかし、コンパイラは原則として揮発性変数をキャッシュすべきではありませんよね?
いいえ、原則として、変数を読み書きするたびに、コンパイラは変数のアドレスを読み書きする必要があります。
[編集: 少なくとも、そのアドレスの値が「観測可能」であると実装が信じるまでは、そうしなければなりません。ディートマーが彼の回答で指摘しているように、実装は通常のメモリを「観察できない」と宣言する可能性があります。これは、デバッガー、mprotect
、または標準の範囲外のものを使用している人にとっては驚きですが、原則として準拠する可能性があります。]
スレッドをまったく考慮しない C++03 では、スレッドでの実行時に「アドレスへのアクセス」が何を意味するかを定義するのは実装次第です。このような詳細は「メモリ モデル」と呼ばれます。たとえば、pthreads では、揮発性変数を含むメモリ全体をスレッドごとにキャッシュできます。IIRC、MSVC は、適切なサイズの揮発性変数がアトミックであることを保証し、キャッシュを回避します (むしろ、すべてのコアに対して単一のコヒーレント キャッシュまでフラッシュします)。その保証を提供する理由は、Intel でそうするのが合理的に安価だからです。Windows は Intel ベースのアーキテクチャだけを本当に気にかけますが、Posix はよりエキゾチックなものに関心があります。
C++11 は、スレッド化のためのメモリ モデルを定義しており、これはデータ競合であると述べています (つまり、あるスレッドでの読み取りが別のスレッドでの書き込みに対して相対的に順序付けられることを保証しvolatile
ません)。2 つのアクセスは、特定の順序で順序付けされるか、不特定の順序で順序付けられます (標準では「不確定な順序」と言うかもしれませんが、思い出せません)、またはまったく順序付けされません。まったくシーケンス化されていないのは悪いことです。2 つのシーケンス化されていないアクセスのいずれかが書き込みである場合、動作は未定義です。
ここで重要なのは、「スレッドから要素を変更し、それを読んでいるスレッドが変更に気付かない」という暗黙の「その後」です。操作が順序付けられていると想定していますが、そうではありません。読み取りスレッドに関する限り、ある種の同期を使用しない限り、他のスレッドでの書き込みがまだ行われていないとは限りません。そして実際にはそれよりも悪い - 私が今書いたことから、指定されていないのは操作の順序だけだと思うかもしれませんが、実際にはデータ競合のあるプログラムの動作は未定義です.