コンピュータプログラミングにおいて、変数の値が現在の実行スレッド以外の何らかのプロセスによって非同期的に読み取られたり変更されたりする場合、その変数は揮発性であると言われます。変数の値は、他のスレッドとの値の共有、非同期シグナルハンドラとの値の共有、メモリマップドI/Oによるハードウェアデバイスへのアクセス(周辺機器からのメッセージをメモリの読み書きによって送受信できる)などの理由で、自発的に変化する可能性があります。これらのユースケースに対するサポートは、キーワードを持つプログラミング言語によって大きく異なります。揮発性は、関数呼び出しの規約や、変数の格納、アクセス、キャッシュの方法に影響を与える可能性があります。volatilevolatile
C および C++ では、は のような型修飾子volatileであり、型の一部です(たとえば、変数またはフィールドの型)。const
C および C++ におけるキーワードの動作は、最適化コンパイラvolatileの最適化を抑制するという観点から説明されることがあります。具体的には、1. 既存の読み取りと書き込みを削除しない、2. 新しい読み取りと書き込みを追加しない、3. 読み取りと書き込みの順序を変更しない、というものです。しかし、この定義は初心者向けの説明に過ぎず、実際の運用コードを書く際にこの定義に頼るべきではありません。volatilevolatilevolatile
C言語、そしてC++言語では、このvolatileキーワードは次のような目的で使用されていました。[ 1 ]
longjmp。volatilesig_atomic_t。C および C++ 規格では、オブジェクト間longjmpで値を共有する移植性の高いコードの記述が認められておりvolatile、シグナル ハンドラとオブジェクト内のその他のコード間で値を共有する移植性の高いコードの記述も認められていますvolatilesig_atomic_t。C および C++ におけるキーワードのその他の使用はvolatile、本質的に移植性がなく、誤りです。特に、メモリ マップド I/Ovolatileデバイスに対してキーワードを使用してコードを記述することは、本質的に移植性がなく、常に特定のターゲット C/C++ 実装とプラットフォームに関する深い知識が必要となります。
C および C++ の移植可能なマルチスレッドvolatileコードではキーワードが有用であるという誤解がよくあります。Cおよび C++ のキーワードは、マルチスレッド シナリオで有用な移植可能なツールとして機能したことはありません。[ 2 ] [ 3 ] [ 4 ] [ 5 ] JavaやC #プログラミング言語とは異なり、 C および C++ の変数に対する操作はアトミックではなく、変数に対する操作には十分なメモリ順序保証 (つまりメモリ バリア)がありません。ほとんどの C および C++ コンパイラ、リンカ、およびランタイムは、キーワードをマルチスレッド シナリオで有用にするために必要なメモリ順序保証を提供していません。C11 およびC++11規格以前は、プログラマはマルチスレッドコードを書くために、個々の実装およびプラットフォーム (POSIX および WIN32 など) からの保証に頼らざるを得ませんでした。最新の C11 および C++11 規格では、プログラマはテンプレートなどの新しい移植可能な構造を使用して移植可能なマルチスレッド コードを書くことができます。 [ 6 ]volatilevolatilevolatilevolatilestd::atomic<T>
fooこの例では、コードはに格納されている値をに設定します0。その後、その値がに変わるまで繰り返しポーリング255を開始します。
static int foo ;void bar ( void ) { foo = 0 ;while ( foo != 255 ) {} }最適化コンパイラは、他のコードがに格納されている値を変更することは不可能であることを認識し、常にとfoo等しいままであると想定します。そのため、コンパイラは関数本体を次のような無限ループに置き換えます。0
void bar_optimized ( void ) { foo = 0 ;while ( true ) {} }ただし、プログラマは、CPUに接続されたデバイスのハードウェア レジスタfooなど、コンピュータ システムの別の要素を参照するように設定できます。この要素は、このコードの実行中にの値を変更する可能性があります。(この例では、CPU に接続されたデバイスのハードウェア レジスタを参照するように設定する方法の詳細は含まれていません。)キーワードがない場合、最適化コンパイラは、ループ内に読み取りがある最初のサンプルのコードを、ループ内に読み取りがない 2 番目のサンプルに変換する可能性が高く、これは一般的なループ不変コード モーション最適化の一部であるため、コードは待機している変更に気づかない可能性が高くなります。foofoovolatile
コンパイラがこの最適化を行わないようにするには、次のvolatileキーワードを使用できます。
static volatile int foo ;void bar ( void ) { foo = 0 ;while ( foo != 255 ) {} }このvolatileキーワードは、コンパイラが読み取りをループの外に移動することを防ぎ、そのためコードは変数に対する期待される変更を認識しますfoo。
以下のC言語プログラムとそれに付随するアセンブリ言語の抜粋は、volatileキーワードがコンパイラの出力にどのように影響するかを示しています。この場合のコンパイラはGCCです。
アセンブリコードを見ると、オブジェクトを使用して生成されたコードはvolatile冗長で長くなり、volatileオブジェクトの性質を満たすようになっていることがはっきりとわかります。volatileキーワードは、コンパイラがvolatileオブジェクトを含むコードに対して最適化を実行するのを防ぎ、volatile変数への代入と読み取りごとに対応するメモリアクセスが行われるようにします。volatileキーワードがない場合、他のスレッドやプロセスからメモリ位置への書き込みがないため、コンパイラは変数を使用するたびにメモリから再読み込みする必要がないことを認識します。
C および C++ の他の言語機能とは異なり、volatileキーワードは、C および C++ 標準に準拠した移植可能な用途であっても、ほとんどの C/C++ 実装で十分にサポートされていません。ほとんどの C/C++ 実装は、volatileキーワードの動作に関してバグがあります。[ 7 ] [ 8 ]プログラマは、volatileC および C++ でキーワードを使用する際には、細心の注意を払う必要があります。
Javaプログラミング言語のすべての最新バージョンにおいて、このvolatileキーワードは以下のことを保証します。
volatile読み取りと書き込みはアトミックです。特に、フィールドへの読み取りと書き込みは、longデータdouble破損を引き起こしません。(アトミック性のvolatile保証は、プリミティブ値または参照値にのみ適用されvolatile、オブジェクト値には適用されません。)volatile。つまり、volatile読み取りでは現在の値(過去や未来の値ではない)が読み取られ、すべてのvolatile読み取りは単一のグローバルな書き込み順序で一致しますvolatile。volatile読み取りと書き込みには、「取得」と「解放」のメモリ バリアセマンティクスがあります (Java 標準ではhappens-beforeとして知られています)。[ 9 ] [ 10 ]言い換えれば、読み取りと書き込みvolatileの相対的な順序について保証を提供します。言い換えれば、基本的に Java のsynchronized ブロックと同じメモリ可視性の保証を提供します(ただし、synchronized ブロックの相互排他保証はありません)。volatilevolatilevolatileこれらの保証が組み合わさることで、Javaではvolatile有用なマルチスレッド構造が実現します。特に、Javaでは、典型的なダブルチェックロックアルゴリズムが正しく動作します。[ 11 ]volatile
Javaバージョン5より前は、Java標準では、volatileメモリバリアのvolatile読み取りと書き込みの相対的な順序が保証されていませんでした。つまり、メモリバリアvolatileには「取得」と「解放」のセマンティクスがありませんでした。このため、マルチスレッド構造としての利用が大きく制限されていました。特に、メモリバリアを使用した一般的なダブルチェックロックアルゴリズムは正しく動作しませんでした。volatile
C#では、volatileフィールドにアクセスするコードが、コンパイラ、CLR、またはハードウェアによって実行される可能性のあるスレッドセーフでない最適化の影響を受けないことを保証します。フィールドが とマークされるとvolatile、コンパイラはフィールドの周囲に「メモリ バリア」または「フェンス」を生成するように指示され、これにより、フィールドに関連付けられた命令の並べ替えやキャッシュが防止されます。volatileフィールドを読み取るとき、コンパイラはacquire-fenceを生成し、これにより、フィールドへの他の読み取りと書き込みがフェンスより前に移動されるのを防ぎます。フィールドに書き込むときvolatile、コンパイラはrelease-fenceを生成します。このフェンスにより、フィールドへの他の読み取りと書き込みがフェンスより後に移動されるのを防ぎます。 [ 12 ]
マークできるのは以下の型のみです。すべての参照型、、、、、、、、、および基底型が、、、、、またはvolatileでSingleあるすべての列挙型。[ 13 ] (これには値構造体、およびプリミティブ型、、、はBoolean含まれません。)ByteSByteInt16UInt16Int32UInt32CharByteSByteInt16UInt16Int32UInt32DoubleInt64UInt64Decimal
キーワードを使用すると、参照渡しされるフィールドやキャプチャされたローカル変数volatileはサポートされません。これらの場合は、代わりにを使用する必要があります。[ 12 ]Thread.VolatileReadThread.VolatileWrite
実際には、これらのメソッドは、通常 C# コンパイラ、JIT コンパイラ、または CPU 自体によって実行される最適化の一部を無効にします。 および によって提供される保証はThread.VolatileRead、キーワードThread.VolatileWriteによって提供される保証のスーパーセットですvolatile。つまり、「ハーフ フェンス」(つまり、acquire-fence はそれより前の命令の並べ替えとキャッシュのみを防止する)を生成する代わりに、VolatileReadおよび は、VolatileWriteそのフィールドの命令の並べ替えとキャッシュを両方向で防止する「フル フェンス」を生成します。[ 12 ]これらのメソッドは次のように動作します。[ 14 ]
Thread.VolatileWriteメソッドは、呼び出し時点でフィールドの値を強制的に書き込むようにします。さらに、それ以前のプログラム順序によるロードとストアは、この呼び出しの前に実行される必要がありVolatileWrite、それ以降のプログラム順序によるロードとストアは、この呼び出しの後に実行される必要があります。Thread.VolatileReadメソッドは、呼び出し時点のフィールドの値を強制的に読み取らせます。さらに、それ以前のプログラム順序によるロードとストアは、この呼び出しの前に実行されなければならずVolatileRead、それ以降のプログラム順序によるロードとストアは、この呼び出しの後に実行されなければなりません。およびメソッドは、メソッドを呼び出すことでフルフェンスを生成しますThread.VolatileRead。このメソッドは、双方向で機能するメモリ バリアを構築します。上記のフルフェンスを使用する動機に加えて、キーワードの潜在的な問題で、によって生成されるフルフェンスを使用することで解決される問題は次のとおりです。ハーフ フェンスの非対称性のため、書き込み命令の後に読み取り命令が続くフィールドでは、コンパイラによって実行順序が入れ替わる可能性があります。フル フェンスは対称であるため、を使用する場合はこの問題は発生しません。[ 12 ]Thread.VolatileWriteThread.MemoryBarriervolatileThread.MemoryBarriervolatileThread.MemoryBarrier
VOLATILEこれはFortran 2003標準の一部ですが、[ 15 ]以前のバージョンでは拡張機能としてサポートされていました。volatile関数内のすべての変数を にすることは、エイリアシング関連のバグを見つけるのにも役立ちます。
integer , volatile :: i ! volatile が定義されていない場合、次の 2 行のコードは同じですwrite ( * , * ) i ** 2 ! 変数 i をメモリから 1 回ロードし、その値を 2 回乗算しますwrite ( * , * ) i * i ! 変数 i をメモリから 2 回ロードし、それらの値を乗算しますFortranコンパイラは、常にVOLATILEのメモリに「ドリルダウン」することで、volatileへの読み書きの順序変更を阻止します。これにより、このスレッドで行われたアクションが他のスレッドから見えるようになり、またその逆も可能になります。[ 16 ]
VOLATILEの使用は最適化を減少させ、場合によっては最適化を阻害する可能性がある。[ 17 ]
{{cite web}}: CS1 maint: bot: 元の URL の状態が不明です (リンク)