web-dev-qa-db-ja.com

volatileがマルチスレッドCまたはC ++プログラミングで有用と見なされないのはなぜですか?

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

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

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

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

156

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

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

共有データへのスレッドセーフアクセスには、次のことを保証する必要があります。

  • 読み取り/書き込みが実際に行われます(コンパイラーは代わりにレジスターに値を保存するだけでなく、メインメモリの更新をずっと後まで延期しません)
  • 並べ替えは行われません。 volatile変数をフラグとして使用して、一部のデータを読み取る準備ができているかどうかを示すと仮定します。このコードでは、データを準備した後にフラグを設定するだけなので、すべてのlooksで問題ありません。しかし、命令が並べ替えられてフラグが設定された場合はどうなりますか?first

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

並べ替えを防ぐための解決策は、メモリバリアを使用することです。これは、コンパイラとCPUの両方にこの点でメモリアクセスを並べ替えることはできませんを示します。揮発性変数アクセスの周囲にこのような障壁を配置することにより、不揮発性アクセスでも揮発性アクセス全体で並べ替えられないため、スレッドセーフコードを記述できます。

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

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

205
jalf

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

Cプログラマーは、変数が現在の実行スレッドの外部で変更される可能性があることを意味するために、しばしば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_datawith()へのアクセスを最適化できません。ロックが保持されている間、shared_dataは揮発性ではありません。共有データを処理する場合、適切なロックにより揮発性が不要になり、潜在的に有害になります。

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

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

while (my_variable != what_i_want)
    cpu_relax();

Cpu_relax()呼び出しは、CPUの消費電力を削減したり、ハイパースレッドツインプロセッサを生成したりできます。また、メモリバリアとして機能することもあるため、揮発性は不要です。もちろん、忙しい待機は一般的に反社会的行為です。

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

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

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

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

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

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

47
user1831086

私はあなたが間違っているとは思わない-値がスレッドA以外の何かによって変更された場合、揮発性はスレッドAが値の変更を確認するために必要です。コンパイラー「この変数をレジスターにキャッシュしないでください。代わりに、アクセスごとにRAM memory)から常に読み取り/書き込みを行ってください。」.

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

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

個人的には、揮発性フラグのメイン(のみ?)使用は、「pleaseGoAwayNow」ブール値としてです。連続的にループするワーカースレッドがある場合、ループの各反復でvolatileブール値をチェックし、ブール値がtrueの場合は終了します。メインスレッドは、ブール値をtrueに設定し、ワーカースレッドがなくなるまでpthread_join()を呼び出して、ワーカースレッドを安全にクリーンアップできます。

11
Jeremy Friesner

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

揮発性変数が持つプロパティは、「この変数からの読み取りと書き込みは、プログラムの知覚可能な動作の一部です」です。つまり、このプログラムは機能します(適切なハードウェアが与えられた場合)。

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-kernelのようなコード、c ++ 0xの同等物を知らない)だけです:

atomic_t counter;

...
atomic_inc(&counter);

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

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

それでも最適化することができます

atomically {
  counter+=2;
}

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

6
jpalecek

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. コンパイラの意味は、assumethatpthread_mutex_lockは共有フラグを変更します
  6. そのため、変数をメモリから再ロードする必要があります。 volatileは、このコンテキストでは意味がありますが、無関係です。
6
Potatoswatter

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

1)原子性、つまりメモリにデータを読み書きする場合、そのデータは1回のパスで読み取り/書き込みが行われ、コンテキスト切り替えなどにより中断または競合することはできません

2)一貫性。つまり、読み取り/書き込み操作の順序は、複数の同時環境間で同じになるようにseenでなければなりません-そのスレッド、マシンなど

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

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

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

c#およびJava AFAIKはvolatileを1)および2)に準拠させることでこれを是正しますが、c/c ++コンパイラについても同じことが言えないため、基本的には適切であると判断してください。

主題に関する詳細な(公平ではないが)議論については this を読んでください。

6
zebrabox

Comp.programming.threads FAQには 古典的な説明 Dave Butenhofによる:

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

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

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

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

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

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

/ --- [Dave Butenhof] ----------------------- [[email protected]] --- \
|デジタル機器株式会社110 Spit Brook Rd ZKO2-3/Q18 |
| 603.881.2218、FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [同時実行性の向上] ---------------- /

Butenhof氏は、 このusenetの投稿 で同じことを扱っています。

「揮発性」の使用は、適切なメモリの可視性またはスレッド間の同期を確保するには不十分です。ミューテックスの使用で十分であり、移植性のないさまざまなマシンコードの代替手段を使用する場合を除き(または、以前の投稿で説明したように、一般に適用するのがはるかに難しいPOSIXメモリルールのより微妙な意味合い)、 mutexは必須です。

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

それはすべてC++にも同様に適用できます。

5
Tony Delroy

これは、「揮発性」が行っているすべてです:「ちょっとコンパイラ、この変数は、ローカルの命令が動作していない場合でも、(任意のクロックティックで)AT ANY MOMENTを変更できます。キャッシュしないでください。レジスタのこの値。」

それだ。これは、コンパイラーに値が揮発性であることを伝えます。この値は外部ロジック(別のスレッド、別のプロセス、カーネルなど)によっていつでも変更される可能性があります。コンパイラーの最適化を抑制するためだけに存在し、コンパイラーの最適化はレジスターに値を静かにキャッシュします。

マルチスレッドプログラミングの万能薬としてvolatileを売り込む「Dr. Dobbs」のような記事に遭遇するかもしれません。彼のアプローチは完全にメリットを欠いているわけではありませんが、オブジェクトのユーザーにスレッドセーフの責任を負わせるという根本的な欠陥があります。

3
Zack Yezek

私の古いC標準によると、「volatile修飾された型を持つオブジェクトへのアクセスを構成するのは実装定義」です。そのため、Cコンパイラライターcouldは、 "volatile"を意味することを選択しました"マルチプロセス環境でのスレッドセーフアクセス"。しかし、そうではありませんでした。

代わりに、マルチコアマルチプロセス共有メモリ環境でクリティカルセクションをスレッドセーフにするために必要な操作が、新しい実装定義機能として追加されました。また、「揮発性」がマルチプロセス環境でアトミックアクセスとアクセス順序を提供するという要件から解放され、コンパイラの作成者は、実装依存の歴史的な「揮発性」セマンティクスよりもコード削減を優先しました。

これは、重要なコードセクションの周りの「揮発性」セマフォのようなものは、新しいコンパイラでは新しいハードウェアでは動作せず、古いハードウェアで古いコンパイラで動作したことがあることを意味します。

3
david