175

最近投稿したこの回答に示されているようにvolatile、マルチスレッド プログラミング コンテキストでのユーティリティ (またはその欠如) について混乱しているようです。

私の理解は次のとおりです。変数にアクセスするコードの制御フローの外部で変数が変更される可能性がある場合はいつでも、その変数は であると宣言する必要がありますvolatile。シグナル ハンドラ、I/O レジスタ、および別のスレッドによって変更された変数はすべて、このような状況を構成します。

したがって、グローバルな intfooがあり、fooあるスレッドによって読み取られ、別のスレッドによって (おそらく適切なマシン命令を使用して) アトミックに設定される場合、読み取りスレッドは、シグナル ハンドラーによって微調整された変数を見るのと同じ方法でこの状況を認識します。外部ハードウェア条件によって変更されるため、foo宣言する必要がありますvolatile(または、マルチスレッドの状況では、おそらくより良い解決策であるメモリ フェンシング ロードでアクセスします)。

どのように、どこで間違っていますか?

4

9 に答える 9

229

volatileマルチスレッド コンテキストの問題は、必要なすべての保証が提供されないことです。必要なプロパティがいくつかありますが、すべてではないため、volatile 単独で依存することはできません。

ただし、残りのプロパティに使用する必要があるプリミティブは、使用するものも提供するため、volatile事実上不要です。

共有データへのスレッドセーフなアクセスには、次の保証が必要です。

  • 読み取り/書き込みが実際に行われます (コンパイラは値をレジスタに格納するだけでなく、メイン メモリの更新をかなり後まで延期します)。
  • 並べ替えが行われないこと。変数をフラグとして使用して、volatileデータを読み取る準備ができているかどうかを示すとします。私たちのコードでは、データを準備した後にフラグを設定するだけなので、すべて問題なく表示されます。しかし、フラグが最初に設定されるように命令が並べ替えられた場合はどうなるでしょうか。

volatile最初のポイントを保証します。また、異なる volatile 読み取り/書き込み間で並べ替えが発生しないことも保証されます。すべてのvolatileメモリ アクセスは、指定された順序で発生します。I/O レジスタまたはメモリ マップド ハードウェアを操作するために必要なのはこれだけですが、オブジェクトが不揮発性データへのアクセスを同期するためにのみ使用されることが多いvolatileマルチスレッド コードでは役に立ちません。volatileこれらのアクセスは、それらのアクセスに対して並べ替えることができvolatileます。

並べ替えを防止する解決策は、メモリ バリアを使用することです。これは、コンパイラと CPU の両方に対して、この時点でメモリ アクセスを並べ替えることができないことを示します。volatile 変数アクセスの周囲にこのようなバリアを配置することで、non-volatile アクセスでさえも volatile アクセスを介して並べ替えられないことが保証され、スレッド セーフなコードを記述できるようになります。

ただし、メモリバリアは、バリアに到達したときに保留中のすべての読み取り/書き込みが実行されることvolatile保証するため、必要なものはすべてそれ自体で効果的に提供され、不要になります。volatile修飾子を完全に削除できます。

C++11 以降、アトミック変数 ( std::atomic<T>) は関連するすべての保証を提供してくれます。

于 2010-03-20T23:17:02.897 に答える
54

Linux Kernel Documentationからこれを検討することもできます。

C プログラマーは、変数が現在の実行スレッドの外部で変更される可能性があることを意味するために volatile を使用することがよくあります。その結果、共有データ構造が使用されているときに、カーネル コードで使用したくなることがあります。言い換えれば、それらは volatile 型を一種の簡単なアトミック変数として扱うことが知られていますが、そうではありません。カーネル コードでの volatile の使用は、ほとんど正しくありません。このドキュメントでは、その理由について説明します。

volatile に関して理解しておくべき重要な点は、その目的が最適化を抑制することであり、これが実際にやりたいことであるということはほとんどないということです。カーネルでは、共有データ構造を不要な同時アクセスから保護する必要がありますが、これはまったく別のタスクです。不要な同時実行から保護するプロセスは、最適化に関連するほぼすべての問題をより効率的な方法で回避します。

volatile と同様に、データへの同時アクセスを安全にするカーネル プリミティブ (スピンロック、ミューテックス、メモリ バリアなど) は、不要な最適化を防ぐように設計されています。それらが適切に使用されている場合、volatile も使用する必要はありません。volatile がまだ必要な場合は、ほぼ確実にコードのどこかにバグがあります。適切に記述されたカーネル コードでは、volatile は動作を遅くするだけです。

カーネル コードの典型的なブロックを考えてみましょう。

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

すべてのコードがロック規則に従っている場合、the_lock が保持されている間、shared_data の値が予期せず変更されることはありません。そのデータを操作する可能性のある他のコードは、ロックで待機します。スピンロック プリミティブは、メモリ バリアとして機能します (そのように明示的に記述されています)。つまり、データ アクセスはそれらをまたがって最適化されません。そのため、コンパイラは shared_data に何が含まれるかを知っていると考えるかもしれませんが、spin_lock() 呼び出しはメモリ バリアとして機能するため、知っていることはすべて強制的に忘れさせます。そのデータへのアクセスに最適化の問題はありません。

shared_data が volatile と宣言された場合でも、ロックは必要です。しかし、他の誰もそれを操作できないことがわかっている場合、コンパイラはクリティカル セクションのshared_data へのアクセスを最適化することもできなくなります。ロックが保持されている間、shared_data は揮発性ではありません。共有データを扱う場合、適切なロックにより volatile が不要になり、有害になる可能性があります。

volatile ストレージ クラスは、もともとメモリ マップド I/O レジスタ用でした。カーネル内では、レジスタ アクセスもロックで保護する必要がありますが、コンパイラがクリティカル セクション内のレジスタ アクセスを「最適化」することも望ましくありません。ただし、カーネル内では、I/O メモリ アクセスは常にアクセサ関数を介して行われます。ポインタを介して I/O メモリに直接アクセスすることは嫌われており、すべてのアーキテクチャで機能するとは限りません。これらのアクセサーは不要な最適化を防ぐために作成されているため、ここでも volatile は不要です。

volatile を使用したくなるもう 1 つの状況は、プロセッサが変数の値を待機中の場合です。ビジー待機を実行する正しい方法は次のとおりです。

while (my_variable != what_i_want)
    cpu_relax();

cpu_relax() 呼び出しは、CPU の消費電力を下げるか、ハイパースレッド化されたツイン プロセッサに譲ることができます。それはたまたまメモリバリアとしても機能するため、繰り返しになりますが、volatile は不要です。もちろん、ビジー ウェイティングは一般的に、そもそも反社会的行為です。

カーネルで volatile が意味を持つまれな状況がまだいくつかあります。

  • 上記のアクセサ関数は、ダイレクト I/O メモリ アクセスが機能するアーキテクチャで volatile を使用する場合があります。基本的に、各アクセサー呼び出しはそれ自体で少し重要なセクションになり、プログラマーの期待どおりにアクセスが行われるようにします。

  • メモリを変更するが、他に目に見える副作用がないインライン アセンブリ コードは、GCC によって削除されるリスクがあります。volatile キーワードを asm ステートメントに追加すると、この削除を防ぐことができます。

  • jiffies 変数は、参照されるたびに異なる値を持つことができるという点で特別ですが、特別なロックなしで読み取ることができます。したがって、jiffies は揮発性になる可能性がありますが、このタイプの他の変数の追加は強く嫌われます。この点で、Jiffies は「愚かな遺産」の問題 (Linus の言葉) と見なされます。それを修正することは、価値があるよりも面倒です。

  • I/O デバイスによって変更される可能性のあるコヒーレント メモリ内のデータ構造へのポインターは、合法的に揮発性になる場合があります。このタイプの状況の例としては、ネットワーク アダプターによって使用されるリング バッファーが挙げられます。この場合、そのアダプターはポインターを変更して、どの記述子が処理されたかを示します。

ほとんどのコードでは、上記の volatile の正当化はどれも当てはまりません。その結果、volatile の使用はバグと見なされる可能性が高く、コードをさらに精査する必要があります。volatile を使用したくなる開発者は、一歩下がって、自分が本当に達成しようとしていることについて考えるべきです。

于 2010-03-21T02:59:42.327 に答える
14

私はあなたが間違っているとは思わない - volatile は、値がスレッド A 以外のものによって変更された場合、スレッド A が値の変更を確認することを保証するために必要です。私が理解しているように、volatile は基本的にコンパイラは、「この変数をレジスタにキャッシュしないでください。代わりに、アクセスのたびに常に RAM メモリから読み書きするようにしてください」。

混乱するのは、多くのことを実装するには volatile では不十分だからです。特に、最新のシステムは複数レベルのキャッシングを使用し、最新のマルチコア CPU は実行時に高度な最適化を行い、最新のコンパイラーはコンパイル時に高度な最適化を行います。ソースコードを見ただけで予想される順序から順番に並べてください。

したがって、volatile 変数の「観察された」変更が、あなたが考えている正確な時間に発生しない可能性があることを念頭に置いている限り、volatile は問題ありません。具体的には、確実に動作しないため、スレッド間で操作を同期または順序付けする方法として揮発性変数を使用しようとしないでください。

個人的には、volatile フラグの主な (唯一の?) 用途は、"pleaseGoAwayNow" ブール値です。継続的にループするワーカー スレッドがある場合は、ループの反復ごとに volatile ブール値をチェックし、ブール値が true の場合は終了します。その後、メイン スレッドは、ブール値を true に設定し、pthread_join() を呼び出してワーカー スレッドがなくなるまで待機することにより、ワーカー スレッドを安全にクリーンアップできます。

于 2010-03-20T22:19:57.487 に答える
9

volatileスピンロック ミューテックスの基本的な構造を実装するには (不十分ではありますが) 便利ですが、それ (またはそれよりも優れたもの) があれば、別の は必要ありませんvolatile

マルチスレッド プログラミングの一般的な方法は、すべての共有変数をマシン レベルで保護するのではなく、プログラム フローをガイドするガード変数を導入することです。代わりにvolatile bool my_shared_flag;あなたが持っているべきです

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

これは「難しい部分」をカプセル化するだけでなく、基本的に必要です。C にはミューテックスの実装に必要なアトミック操作が含まれていません。通常の操作volatileについて追加の保証を行うだけで済みます。

今、あなたはこのようなものを持っています:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flagキャッシュ不可であっても、揮発性である必要はありません。

  1. 別のスレッドがそれにアクセスできます。
  2. &それへの参照は、いつか(オペレーターによって) 取得されたに違いないことを意味します。
    • (または、包含構造への参照が取られました)
  3. pthread_mutex_lockライブラリ関数です。
  4. つまり、コンパイラは、何らかの方法pthread_mutex_lockでその参照を取得するかどうかを判断できません。
  5. つまり、コンパイラは共有フラグを変更することを想定する必要があります!pthread_mutex_lock
  6. そのため、変数をメモリから再ロードする必要があります。volatile、このコンテキストでは意味がありますが、無関係です。
于 2010-03-20T23:18:09.367 に答える
7

あなたの理解は本当に間違っています。

揮発性変数が持つプロパティは、「この変数からの読み取りとこの変数への書き込みは、プログラムの認識可能な動作の一部です」です。これは、このプログラムが機能することを意味します(適切なハードウェアがあれば):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

問題は、これはスレッドセーフなものに必要なプロパティではないということです。

たとえば、スレッドセーフカウンターは次のようになります(Linuxカーネルのようなコード、c ++ 0xに相当するものはわかりません)。

atomic_t counter;

...
atomic_inc(&counter);

これはアトミックであり、メモリバリアはありません。必要に応じて追加する必要があります。volatileを追加しても、近くのコードへのアクセスが関連付けられないため(たとえば、カウンターがカウントしているリストに要素を追加する場合)、おそらく役に立ちません。確かに、プログラムの外部でカウンターがインクリメントされるのを確認する必要はありません。たとえば、最適化は依然として望ましいものです。

atomic_inc(&counter);
atomic_inc(&counter);

まだ最適化することができます

atomically {
  counter+=2;
}

オプティマイザーが十分に賢い場合(コードのセマンティクスは変更されません)。

于 2010-03-20T22:43:18.467 に答える
6

並行環境でデータの一貫性を保つには、次の 2 つの条件を適用する必要があります。

1) 原子性、つまり、メモリにデータを読み書きする場合、そのデータは 1 回のパスで読み書きされ、コンテキスト スイッチなどによって中断または競合することはありません。

2) 一貫性、つまり読み取り/書き込み操作の順序は、スレッド、マシンなど、複数の同時実行環境間で同じである 必要があります

volatile は上記のいずれにも適合しません。より具体的には、volatile の動作方法に関する c または c++ 標準には、上記のいずれも含まれていません。

実際には、一部のコンパイラ (インテル Itanium コンパイラなど) が同時アクセスの安全な動作の要素を実装しようとする (つまり、メモリ フェンスを確保することによって) ため、さらに悪いことになりますが、コンパイラの実装全体で一貫性がなく、さらに標準ではこれを必要としません。そもそも実装の。

変数を volatile としてマークすることは、値を毎回メモリとの間で強制的にフラッシュすることを意味するだけであり、多くの場合、基本的にキャッシュのパフォーマンスが低下するため、コードの速度が低下します。

c# と Java AFAIK は、volatile を 1) と 2) に準拠させることでこれを修正しますが、c/c++ コンパイラについては同じことが言えないため、基本的には適切と思われる方法で行います。

このテーマに関するより詳細な (偏りのない) 議論については、これをお読みください。

于 2010-03-21T01:28:08.593 に答える
6

comp.programming.threads FAQ には、 Dave Butenhof による古典的な説明があります。

Q56: 共有変数 VOLATILE を宣言する必要がないのはなぜですか?

ただし、コンパイラとスレッド ライブラリの両方がそれぞれの仕様を満たしている場合が懸念されます。準拠する C コンパイラは、CPU がスレッドからスレッドに渡されるときに保存および復元されるレジスタに、一部の共有 (不揮発性) 変数をグローバルに割り当てることができます。各スレッドは、この共有変数に対して独自のプライベート値を持ちますが、これは共有変数に必要なものではありません。

ある意味では、コンパイラが変数と pthread_cond_wait (または pthread_mutex_lock) 関数のそれぞれのスコープについて十分に認識している場合、これは真実です。実際には、ほとんどのコンパイラは、外部関数の呼び出し全体でグローバル データのレジスタ コピーを保持しようとはしません。これは、ルーチンがデータのアドレスに何らかの方法でアクセスできるかどうかを知るのが非常に難しいためです。

確かに、ANSI C に厳密に (ただし非常に積極的に) 準拠しているコンパイラは、volatile を使用しない複数のスレッドでは動作しない可能性があります。しかし、誰かがそれを修正した方がよいでしょう。POSIX メモリの一貫性を保証しないシステム (つまり、実用的には、カーネル、ライブラリ、および C コンパイラの組み合わせ) は、POSIX 標準に準拠していないためです。限目。POSIX は POSIX 同期関数が必要であることのみを要求するため、正しい動作のために共有変数で volatile を使用することをシステムが要求することはできません。

したがって、volatile を使用しなかったためにプログラムが壊れた場合、それはバグです。C のバグでも、スレッド ライブラリのバグでも、カーネルのバグでもない可能性があります。しかし、これはシステムのバグであり、修正するにはこれらのコンポーネントの 1 つ以上が機能する必要があります。

volatile を使用したくないのは、それが違いを生むシステムでは、適切な不揮発性変数よりもはるかに高価になるからです。(ANSI C では、各式で volatile 変数に「シーケンス ポイント」が必要ですが、POSIX では同期操作でのみ必要です。計算集約型のスレッド化されたアプリケーションでは、volatile を使用するメモリ アクティビティが大幅に増加します。本当に遅くなります。)

/---[デイブ ブテンホフ]-----------------------[butenhof@zko.dec.com]---\
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3/Q18 |
| | 603.881.2218、FAX 603.881.0120 ナシュア NH 03062-2698 |
-----------------[並行処理によるより良い生活]----------------/

ブテンホフ氏は、このユースネットの投稿で同じ分野の多くをカバーしています。

「揮発性」の使用は、適切なメモリの可視性またはスレッド間の同期を保証するのに十分ではありません。ミューテックスの使用は十分であり、さまざまな移植性のないマシンコードの代替手段に頼ることを除いて (または、私の以前の投稿で説明したように、一般的に適用するのがはるかに難しい POSIX メモリ規則のより微妙な影響)、ミューテックスが必要です。

したがって、Bryan が説明したように、volatile の使用は、コンパイラが有用で望ましい最適化を行うのを妨げるだけであり、コードを「スレッドセーフ」にするのに何の助けにもなりません。もちろん、何でも「揮発性」として宣言することは大歓迎です。結局のところ、これは正当な ANSI C ストレージ属性です。スレッド同期の問題が解決されるとは思わないでください。

これらはすべて、C++ にも同様に当てはまります。

于 2010-10-05T08:05:10.667 に答える
3
于 2014-11-14T11:34:30.070 に答える