9

読み取り、書き込み、保留中の読み取り、保留中の書き込みカウンターを保持するマルチ R/W ロック クラスがあります。ミューテックスは、それらを複数のスレッドから保護します。

私の質問は、コンパイラーが最適化の実行中にカウンターを台無しにしないように、カウンターを揮発性として宣言する必要がありますか?

または、コンパイラは、カウンターがミューテックスによって保護されていることを考慮していますか。

ミューテックスは同期のための実行時のメカニズムであり、「volatile」キーワードは、最適化を行いながら正しいことを行うためのコンパイラへのコンパイル時の指示であることを理解しています。

よろしく、 -ジェイ。

4

5 に答える 5

16

ここには、基本的に無関係な 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++ 標準ではまだスレッドについて言及されていませんが、これらのコンパイラでは (少なくとも最低限) 言及されています。

だから、短い答え

揮発性は必要ありません。

于 2009-11-05T17:41:14.900 に答える
14

Herb Sutter の記事「クリティカル セクション (できればロック) を使用して競合を排除する」( http://www.ddj.com/cpp/201804238 ) から:

したがって、並べ替え変換が有効であるためには、クリティカル セクションの 1 つの重要な規則に従うことによって、プログラムのクリティカル セクションを尊重する必要があります。コードはクリティカル セクションから出ることはできません。(コードの移動はいつでも問題ありません。) クリティカル セクションの開始と終了に対称的な一方向フェンス セマンティクスを要求することで、このゴールデン ルールを適用します (図 1 の矢印で示しています)。

  • クリティカル セクションに入ることは、取得操作または暗黙の取得フェンスです。コードはフェンスを上方向に越えることはできません。つまり、フェンスの後の元の場所からフェンスの前に実行するために移動することはできません。ただし、ソース コードの順序でフェンスの前に表示されるコードは、後で実行するためにフェンスを下に横切ることができます。
  • クリティカル セクションを終了することはリリース操作、または暗黙のリリース フェンスです。これは、コードが下向きにフェンスを越えることはできず、上向きにのみフェンスを越えることができないという逆の要件です。最終リリースの書き込みを確認する他のスレッドも、それより前のすべての書き込みを確認することが保証されます。

そのため、クリティカル セクションに出入りするときに、コンパイラがターゲット プラットフォーム用の正しいコードを生成するために (クリティカル セクションという用語は、構造によって保護されているものの Win32 の意味で必ずしも使用されるわけではなく、一般的な意味で使用されますCRITICAL_SECTION- クリティカル セクション)他の同期オブジェクトで保護できます) 正しい取得および解放セマンティクスに従う必要があります。したがって、保護されたクリティカル セクション内でのみアクセスされる限り、共有変数を揮発性としてマークする必要はありません。

于 2009-10-23T22:52:43.363 に答える
5

volatileは、ロケーションの現在の値をレジスターにロードして変更されないと想定するのではなく、常にロケーションの現在の値をロードするようにオプティマイザーに通知するために使用されます。これは、デュアルポートメモリの場所、またはスレッドの外部のソースからリアルタイムで更新できる場所を操作する場合に最も役立ちます。

ミューテックスは、コンパイラが実際には何も知らないランタイムOSメカニズムであるため、オプティマイザはそれを考慮しません。一度に複数のスレッドがカウンターにアクセスするのを防ぎますが、ミューテックスが有効になっている間でも、それらのカウンターの値は変更される可能性があります。

つまり、変数を揮発性としてマークしているのは、それらがミューテックスガード内にあるためではなく、外部から変更できるためです。

それらを揮発性に保ちます。

于 2009-10-23T22:06:27.867 に答える
4

これは使用しているスレッド ライブラリに依存する場合がありますが、まともなライブラリvolatile.

たとえば、 Pthreads では、ミューテックスを使用すると、データがメモリに正しくコミットされます。

編集:私はここに、トニーの答えが自分の答えよりも優れていること を支持します。

于 2009-10-23T22:51:19.523 に答える
3

まだ「volatile」キーワードが必要です。

ミューテックスは、カウンターが同時にアクセスするのを防ぎます。

「volatile」は、CPUレジスター(並行スレッドによって更新されない)にカウンターをキャッシュする代わりに、実際にカウンターを使用するようにコンパイラーに指示します。

于 2009-10-23T21:55:20.913 に答える