ここには、基本的に無関係な 2 つの項目があり、常に混同されています。
volatile は、レジスターからではなくメモリーから変数を読み取るコードを生成するようにコンパイラーに指示するために使用されます。そして、コードを並べ替えないでください。一般に、「近道」を最適化したり実行したりしないでください。
別の回答で Herb Sutter から引用されているように、メモリバリア (ミューテックス、ロックなどによって提供される) は、コンパイラがどのように実行するように指示したかに関係なく、CPUがメモリの読み取り/書き込み要求を並べ替えるのを防ぐためのものです。つまり、CPU レベルで、最適化しないでください。近道をしないでください。
似ていますが、実際には非常に異なるものです。
あなたの場合、そしてほとんどのロックの場合、 volatile が必要ない理由は、ロックのために関数呼び出しが行われているためです。すなわち:
最適化に影響する通常の関数呼び出し:
external void library_func(); // from some external library
global int x;
int f()
{
x = 2;
library_func();
return x; // x is reloaded because it may have changed
}
コンパイラーが library_func() を調べて x に触れていないと判断できない限り、戻り時に x を再読み取りします。これは揮発性なしでもあります。
スレッド:
int f(SomeObject & obj)
{
int temp1;
int temp2;
int temp3;
int temp1 = obj.x;
lock(obj.mutex); // really should use RAII
temp2 = obj.x;
temp3 = obj.x;
unlock(obj.mutex);
return temp;
}
temp1 の obj.x を読み取った後、コンパイラは temp2 の obj.x を再度読み取ります。これは、ロックの魔法のためではなく、lock() が obj を変更したかどうか不明なためです。おそらく、コンパイラ フラグを積極的に最適化 (エイリアスなしなど) するように設定して、x を再読み込みしないようにすることもできますが、その場合、多くのコードが失敗し始める可能性があります。
temp3 の場合、コンパイラは (うまくいけば) obj.x を再読み込みしません。何らかの理由で obj.x が temp2 と temp3 の間で変更される可能性がある場合は、volatile を使用します (そして、ロックが壊れているか役に立たないでしょう)。
最後に、lock()/unlock() 関数が何らかの形でインライン化されている場合、コンパイラはコードを評価し、obj.x が変更されていないことを確認できます。ただし、ここでは次の 2 つのいずれかを保証します: - インライン コードが最終的に OS レベルのロック関数を呼び出す (したがって、評価が妨げられる) または - コンパイラが認識する asm メモリ バリア命令 (つまり、__InterlockedCompareExchange などのインライン関数にラップされている) を呼び出します。したがって、並べ替えを回避します。
編集: PS 言及するのを忘れていました - pthreads の場合、一部のコンパイラは「POSIX 準拠」としてマークされています。これは、とりわけ、それらが pthread_ 関数を認識し、それらの周りで悪い最適化を行わないことを意味します。つまり、C++ 標準ではまだスレッドについて言及されていませんが、これらのコンパイラでは (少なくとも最低限) 言及されています。
だから、短い答え
揮発性は必要ありません。