C/Pスレッドの使用:共有変数は揮発性である必要がありますか?

StackOverflow https://stackoverflow.com/questions/78172

  •  09-06-2019
  •  | 
  •  

質問

C プログラミング言語とスレッド ライブラリとしての Pthreads。スレッド間で共有される変数/構造体は volatile として宣言する必要がありますか?ロック (おそらく障壁) で保護されているかどうかを前提としています。

pthread POSIX 標準はこれに関して何らかの発言権を持っていますか、これはコンパイラに依存しているのでしょうか、それともどちらでもないのでしょうか?

編集して追加します:素晴らしい回答をありがとうございました。でも、もしあなたがそうならどうしますか ない ロックの使用。を使用している場合はどうなりますか 障壁 例えば?または、次のようなプリミティブを使用するコード 比較交換 共有変数を直接かつアトミックに変更するには...

役に立ちましたか?

解決

volatile の非常に重要な特性の 1 つは、変更時に変数がメモリに書き込まれ、アクセスされるたびにメモリから再度読み取られることだと思います。ここでの他の回答は揮発性と同期を混合していますが、これ以外のいくつかの回答から、揮発性は同期プリミティブではないことは明らかです(クレジットが必要な場合はクレジット)。

ただし、volatile を使用しない限り、コンパイラーは共有データを任意の期間自由にレジスターにキャッシュできます。データをコンパイラの判断でレジスタにキャッシュするだけでなく、実際のメモリに予測どおりに書き込むようにしたい場合は、そのデータを揮発性としてマークする必要があります。あるいは、共有データを変更する関数を終了した後でのみ共有データにアクセスする場合は、問題ない可能性があります。ただし、値がレジスタからメモリに確実に書き戻されるように、やみくもに運に頼らないことをお勧めします。

特にレジスターが豊富なマシン (つまり、x86 ではない) では、変数はレジスター内で長期間存続する可能性があり、優れたコンパイラーは構造体の一部または全体をレジスター内にキャッシュできます。したがって、volatile を使用する必要がありますが、パフォーマンスを考慮して、計算のために値をローカル変数にコピーしてから、明示的にライトバックを実行します。基本的に、volatile を効率的に使用するということは、C コードでロードとストアの考え方を少し行うことを意味します。

いずれの場合でも、正しいプログラムを作成するには、OS レベルで提供されるある種の同期メカニズムを積極的に使用する必要があります。

volatile の弱点の例については、私の Decker のアルゴリズムの例を参照してください。 http://jakob.engbloms.se/archives/65, これは、volatile が同期に機能しないことをかなりよく証明しています。

他のヒント

ロックを使用して変数へのアクセスを制御している限り、それに volatile を設定する必要はありません。実際、変数に volatile を設定している場合、おそらくすでに間違っています。

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

答えは絶対に、明白に「NO」です。適切な同期プリミティブに加えて「volatile」を使用する必要はありません。実行する必要があることはすべて、これらのプリミティブによって実行されます。

「揮発性」の使用は必要でも十分でもありません。適切な同期プリミティブで十分であるため、これは必要ありません。これでは、問題となる可能性のあるすべての最適化が無効になるわけではなく、一部の最適化が無効になるだけなので十分ではありません。たとえば、別の CPU でのアトミック性や可視性は保証されません。

ただし、volatile を使用しない限り、コンパイラーは共有データを任意の期間自由にレジスターにキャッシュできます。データをコンパイラの判断でレジスタにキャッシュするだけでなく、実際のメモリに予測どおりに書き込むようにしたい場合は、そのデータを揮発性としてマークする必要があります。あるいは、共有データを変更する関数を終了した後でのみ共有データにアクセスする場合は、問題ない可能性があります。ただし、値がレジスタからメモリに確実に書き戻されるように、やみくもに運に頼らないことをお勧めします。

そうですね。ただし、volatile を使用する場合でも、CPU は任意の期間、書き込みポスト バッファーに共有データを自由にキャッシュできます。厄介な最適化のセットは、「揮発性」によって無効化される最適化のセットと正確には同じではありません。したがって、「volatile」を使用すると、 盲目的な運に頼る。

一方、定義されたマルチスレッド セマンティクスで同期プリミティブを使用すると、確実に動作します。プラスとして、「揮発性」によるパフォーマンスへの大きな影響を受けなくなります。では、なぜそのようにしてはいけないのでしょうか?

キーワード volatile はマルチスレッド プログラミングに適しているという考えが広く普及しています。

ハンス・ベーム 指摘している volatile にはポータブルな用途が 3 つしかないということです。

  • 揮発性の setjmp と同じスコープ内のローカル変数をマークするために使用できます。その値は、longjmp 全体にわたって保持される必要があります。問題のローカル変数を共有する方法がない場合、アトミック性と順序付けの制約は効果がないため、そのような使用のどの部分が遅くなるかは不明です。(longjmp 全体ですべての変数を保持する必要があることによって、そのような使用のどの部分が遅くなるかさえ不明ですが、それは別の問題であり、ここでは考慮されません。)
  • 揮発性の 変数が「外部変更」される可能性がある場合に使用できますが、実際の変更はスレッド自体によって同期的にトリガーされます。基礎となるメモリが複数の場所にマップされているためです。
  • 揮発性の sigatomic_t は、制限された方法で同じスレッド内のシグナル ハンドラーと通信するために使用できます。sigatomic_t ケースの要件を弱めることを検討することもできますが、それはかなり直観に反するように思えます。

あなたがいる場合 マルチスレッド 速度を上げるために、コードを遅くすることは決して望ましいことではありません。マルチスレッド プログラミングの場合、揮発性が解決できると誤解されがちな 2 つの重要な問題があります。

  • 原子性
  • 記憶の一貫性, 、つまり別のスレッドから見たスレッドの操作の順序。

まずは(1)から考えてみましょう。Volatile はアトミックな読み取りまたは書き込みを保証しません。たとえば、129 ビット構造の揮発性読み取りまたは書き込みは、ほとんどの最新のハードウェアではアトミックになりません。32 ビット int の揮発性読み取りまたは書き込みは、最新のハードウェアではアトミックですが、 揮発性はそれとは何の関係もありません. 。揮発性がなければアトミックになる可能性があります。アトミック性はコンパイラの気まぐれに左右されます。C または C++ 標準には、アトミックでなければならないという規定はありません。

次に問題 (2) について考えてみましょう。プログラマは、volatile を volatile アクセスの最適化をオフにすることと考えることがあります。実際にはそれがほぼ当てはまります。ただし、これは揮発性アクセスのみであり、不揮発性アクセスではありません。この断片を考えてみましょう。

 volatile int Ready;       

    int Message[100];      

    void foo( int i ) {      

        Message[i/10] = 42;      

        Ready = 1;      

    }

マルチスレッドプログラミングにおいて非常に合理的なことを行おうとしています。メッセージを書いて別のスレッドに送信します。もう一方のスレッドは、Ready がゼロ以外になるまで待機してから、メッセージを読み取ります。gcc 4.0 または icc を使用して、「gcc -O2 -S」でこれをコンパイルしてみてください。どちらも最初に Ready へのストアを実行するため、i/10 の計算と重複する可能性があります。並べ替えはコンパイラのバグではありません。これは、その仕事を行う積極的なオプティマイザーです。

解決策は、すべてのメモリ参照を揮発性としてマークすることだと思うかもしれません。それはまったく愚かなことです。前述の引用にあるように、コードの速度が低下するだけです。さらに最悪の場合、問題は解決しない可能性があります。コンパイラが参照の順序を変更しなくても、ハードウェアが参照の順序を変更する可能性があります。この例では、x86 ハードウェアは順序を変更しません。Itanium(TM) プロセッサも同様です。Itanium コンパイラは揮発性ストアにメモリ フェンスを挿入するためです。これは賢い Itanium 拡張機能です。しかし、Power(TM) のようなチップは並べ替えられます。ご注文の際に必ず必要なものは、 メモリーフェンス, 、 とも呼ばれている 記憶の壁. 。メモリ フェンスは、フェンスを越えたメモリ操作の並べ替えを防ぎ、場合によっては一方向の並べ替えを防ぎます。Volatile はメモリ フェンスとは関係ありません。

では、マルチスレッドプログラミングの解決策は何でしょうか?アトミックおよびフェンスのセマンティクスを実装するライブラリまたは言語拡張機能を使用します。意図したとおりに使用すると、ライブラリ内の操作によって適切なフェンスが挿入されます。いくつかの例:

  • POSIX スレッド
  • Windows(TM) スレッド
  • OpenMP
  • 未定

に基づく Arch Robison (インテル) による記事

私の経験では、そうではありません。これらの値に書き込むときに自分自身を適切にミューテックスするか、別のスレッドのアクションに依存するデータにアクセスする前にスレッドが停止するようにプログラムを構築する必要があります。私のプロジェクト x264 ではこの方法を使用しています。スレッドは膨大な量のデータを共有しますが、その大部分はミューテックスを必要としません。これは、スレッドが読み取り専用であるか、スレッドがデータにアクセスする必要がある前に、データが利用可能になって完了するまで待機するためです。

さて、操作中にすべてのスレッドが高度にインターリーブされている (非常に細かいレベルで相互の出力に依存している) 多くのスレッドがある場合、これははるかに困難になる可能性があります。実際、そのような場合、私は次のようにします。スレッド モデルを再検討して、スレッド間をさらに分離してよりクリーンに実行できるかどうかを確認してください。

いいえ。

Volatile CPU 読み取り/書き込みコマンドとは独立して変更される可能性があるメモリ位置を読み取る場合にのみ必要です。スレッド化の状況では、CPU が各スレッドのメモリへの読み取り/書き込みを完全に制御するため、コンパイラはメモリがコヒーレントであると想定し、CPU 命令を最適化して不必要なメモリ アクセスを削減します。

主な用途は、 volatile メモリマップド I/O にアクセスするためのものです。この場合、基礎となるデバイスは CPU とは独立してメモリ位置の値を変更できます。使用しない場合 volatile この状況では、CPU は新しく更新された値を読み取る代わりに、以前にキャッシュされたメモリ値を使用する可能性があります。

Volatile は、あるスレッドが何かを書き込んでから別のスレッドがそれを読み取るまでの間に遅延がまったく必要ない場合にのみ役立ちます。ただし、何らかのロックがなければ、何もわかりません。 いつ 他のスレッドがデータを書き込んだのは、それが可能な最新の値であるということだけです。

単純な値 (さまざまなサイズの int と float) の場合、明示的な同期ポイントが必要ない場合、ミューテックスは過剰になる可能性があります。何らかの種類のミューテックスやロックを使用しない場合は、変数を volatile として宣言する必要があります。ミューテックスを使用すれば、準備は完了です。

複雑な型の場合は、ミューテックスを使用する必要があります。これらの操作は非アトミックであるため、ミューテックスなしで半分変更されたバージョンを読み取ることができます。

揮発性とは、この値を取得または設定するためにメモリにアクセスする必要があることを意味します。volatile を設定しない場合、コンパイルされたコードはデータをレジスタに長期間保存する可能性があります。

これが意味するのは、スレッド間で共有する変数を揮発性としてマークする必要があるということです。そうすることで、あるスレッドが値の変更を開始しても、2 番目のスレッドが来て値を読み取ろうとする前にその結果が書き込まれないという状況を避けることができます。 。

Volatile は、特定の最適化を無効にするコンパイラ ヒントです。コンパイラの出力アセンブリはこれなしでも安全であったかもしれませんが、共有値には常にこれを使用する必要があります。

これは、システムが提供する高価なスレッド同期オブジェクトを使用していない場合に特に重要です。たとえば、一連のアトミックな変更によってデータ構造を有効に保つことができる場合があります。メモリを割り当てない多くのスタックは、スタックに値を追加してから終了ポインタを移動したり、終了ポインタを移動した後にスタックから値を削除したりできるため、このようなデータ構造の例です。このような構造を実装する場合、アトミックな命令が実際にアトミックであることを保証するために volatile が重要になります。

根本的な理由は、C 言語のセマンティクスが シングルスレッド抽象マシン. 。そして、コンパイラは、抽象マシン上でプログラムの「観察可能な動作」が変化しない限り、プログラムを変換する独自の権利を有します。隣接または重複するメモリ アクセスをマージしたり、メモリ アクセスを複数回 (レジスタ スピルなどで) やり直したり、実行時にプログラムの動作と考えられる場合には単にメモリ アクセスを破棄したりすることができます。 単一のスレッド, 、変わりません。したがって、ご想像のとおり、その動作は する プログラムが実際にマルチスレッドで実行されることになっている場合は変更します。

ポール・マッケニーが有名な著書で指摘したように、 Linux カーネルのドキュメント:

_must_not_ read_once()およびwrite_once()によって保護されていないメモリ参照でコンパイラが必要なことを行うと想定されます。それらがなければ、コンパイラは、コンパイラバリアセクションでカバーされているあらゆる種類の「創造的な」変革を行う権利の範囲内です。

READ_ONCE() と WRITE_ONCE() は、参照される変数の揮発性キャストとして定義されます。したがって:

int y;
int x = READ_ONCE(y);

は以下と同等です:

int y;
int x = *(volatile int *)&y;

したがって、「揮発性」アクセスを行わない限り、アクセスが確実に行われるという保証はありません。 ちょうど一度だけ, 使用している同期メカニズムに関係なく。外部関数 (pthread_mutex_lock など) を呼び出すと、コンパイラにグローバル変数へのメモリ アクセスが強制される場合があります。ただし、これは、外部関数がこれらのグローバル変数を変更するかどうかをコンパイラが判断できない場合にのみ発生します。高度なプロシージャ間分析とリンク時の最適化を採用した最新のコンパイラでは、このトリックはまったく役に立ちません。

要約すると、複数のスレッドで共有される変数を volatile としてマークするか、volatile キャストを使用して変数にアクセスする必要があります。


ポール・マッケニーも次のように指摘しています。

子どもたちには知られたくない最適化テクニックについて話し合うとき、彼らの目が輝いているのを見てきました。


しかし何が起こるかを見てください C11/C++11.

理解できません。プリミティブを同期すると、コンパイラに変数の値の再ロードがどのように強制されるのでしょうか?なぜ既存の最新コピーを使用しないのでしょうか?

揮発性とは、変数がコードのスコープ外で更新されるため、コンパイラは変数の現在の値を認識していると想定できないことを意味します。コンパイラはメモリ バリアを意識しない (そうですよね?) ため、メモリ バリアさえも役に立ちません。キャッシュされた値を依然として使用する可能性があります。

明らかに、コンパイラが同期呼び出しをメモリ バリアとして扱うと想定している人もいます。「Casey」は、CPU が 1 つだけあると仮定しています。

同期プリミティブが外部関数であり、問​​題のシンボルがコンパイル単位 (グローバル名、エクスポートされたポインター、それらを変更する可能性のあるエクスポートされた関数) の外部で可視である場合、コンパイラーはそれらを、またはその他の外部関数呼び出しを、外部から見えるすべてのオブジェクトに関してメモリフェンスを設定します。

それ以外の場合は、あなたは自分自身で行動することになります。そして、volatile は、コンパイラーに正確で高速なコードを生成させるために利用できる最良のツールである可能性があります。ただし、一般的には移植可能ではありません。揮発性が必要な場合や、実際にどのように機能するかはシステムとコンパイラに大きく依存します。

いいえ。

初め、 volatile 必要ありません。保証されたマルチスレッド セマンティクスを提供する操作は他にも多数あります。 volatile. 。これらには、アトミック操作、ミューテックスなどが含まれます。

2番、 volatile では不十分です。C 標準では、宣言された変数のマルチスレッド動作については保証されていません。 volatile.

したがって、必要でも十分でもないので、使用する意味はあまりありません。

1 つの例外は、マルチスレッド セマンティクスが文書化されている特定のプラットフォーム (Visual Studio など) です。

スレッド間で共有される変数は「volatile」として宣言する必要があります。これにより、1つのスレッドがそのような変数に書き込むと、書き込みは(レジスタとは対照的に)メモリに対してである必要があることをコンパイラに伝えます。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top