今更言うまでもありませんが、C/C++とC#/Javaではキーワードvolatileの意味が若干異なります。
C/C++
C/C++言語のvolatile修飾子は、コンパイラに副作用を示唆し、メモリアクセスの最適化を抑制するために存在します。volatileは典型的な処理系依存機能のうちのひとつであり、解釈は各コンパイラの実装に委ねられています。
C/C++のvolatileは、ときどきマルチスレッド間の簡易的な同期・通信用として使われていることがあります。たとえばサブスレッドでの処理完了を待機するために、グローバル変数などを用いて定義した処理完了フラグをメインスレッドにて監視・ポーリングする、といった状況です。
#include <cstdio> #include <windows.h> #include <process.h> #include <conio.h> volatile bool g_completed; UINT CALLBACK MyThreadFunction(void*) { printf("Begin of sub-thread.\n"); // ヘビーな処理を実行。 for (int i = 0; i < 3; ++i) { printf("Executing task #[%d]...\n", i); ::Sleep(1000); } g_completed = true; printf("End of sub-thread.\n"); return 0; } int main() { printf("Main thread waiting for completion of sub-thread...\n"); auto hThread = reinterpret_cast<HANDLE>(_beginthreadex(nullptr, 0, MyThreadFunction, nullptr, 0, nullptr)); while (!g_completed) { // メッセージ処理などを行ないながら待機。 ::Sleep(1); } ::WaitForSingleObject(hThread, INFINITE); ::CloseHandle(hThread); hThread = nullptr; puts("Press any..."); _getch(); }
※本来はC++11で標準化されたstd::threadを使ってもよいのですが、VC10.0 (VC++2010) などの古い処理系でもコンパイルできるよう、あえて古典的なCRTを使ってみました。
#include <cstdio> #include <thread> #include <chrono> volatile bool g_completed; void Sleep(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } void MyThreadFunction() { printf("Begin of sub-thread.\n"); // ヘビーな処理を実行。 for (int i = 0; i < 3; ++i) { printf("Executing task #[%d]...\n", i); ::Sleep(1000); } g_completed = true; printf("End of sub-thread.\n"); } int main() { printf("Main thread waiting for completion of sub-thread...\n"); std::thread thread(MyThreadFunction); while (!g_completed) { // メッセージ処理などを行ないながら待機。 ::Sleep(1); } thread.join(); puts("Finished."); }
処理完了フラグはサブスレッドで書き換えますが、上記のようなケースにおいてフラグ変数をvolatileで修飾しない場合、コンパイル時の最適化により、フラグ変数へのアクセスを(メインスレッド-サブスレッド間で共有する)メインメモリではなく、(スレッドローカルな)レジスタにて実行するようなコードを出力してしまうことがあります。そうなると上記のポーリング用whileループの継続条件式は常に真となってしまい、メインスレッドはループを永遠に脱出できなくなります(無限ループ)。
ただし、こういったスレッド間の同期・通信用途はC/C++本来のvolatileの守備範囲ではなく、避けるべきです。そもそも、C++03規格およびそれ以前では、スレッドという概念そのものが標準化されていません。このようにvolatileがマルチスレッド同期に乱用されるようになった背景として、MSVCにおける独自の勝手な言語拡張*1があります。
VC++のvolatile拡張仕様では、なんとメモリバリアまで張ってくれるらしいです。つまり読み書き操作が暗黙的にアトミック処理になります。ただしx86/x64アーキテクチャとARMアーキテクチャとでは既定の動作(コンパイラオプション)が異なるらしいので注意が必要です。移植性を考えると、VC++のvolatile拡張仕様に依存するべきではなく、メモリバリアはスレッドライブラリに用意されている同期オブジェクトを使って明示的に実装するべきです。
なお、アトミック操作の実現手段としては、C++11規格以降は基本的に標準ライブラリのstd::atomicを使うべきです。ただし、C++/CLI (/clr) では残念ながらstd::atomicが使えないそうで、代わりに.NET標準クラスライブラリで用意されている同期オブジェクトやアトミック処理用のSystem.Threading.Interlockedクラスなどを使います。処理系依存のvolatileを使うのは最後の手段にしましょう。ちなみに前述の例は、WaitForSingleObject()の第2引数に1を指定して(タイムアウト時間1ミリ秒の待機とする)、ポーリングループ内で呼び出して戻り値をチェックする方法に変更すれば、volatileグローバルフラグ変数を取り除くことができます。
while (::WaitForSingleObject(hThread, 1) == WAIT_TIMEOUT) { // メッセージ処理などを行ないながら待機。 }
C#/Java
C#のvolatileは、VC++のようなメモリバリアまではなされないものの、最適化を抑制する効果があります。C/C++と違い、処理系依存ではなくれっきとした言語仕様となっています。Javaのvolatileと同様です。C#/Javaは当初からマルチスレッドを考慮した言語設計がなされており、volatile仕様に関してもマルチスレッドが考慮されているため、限定的ながらvolatileをスレッド間の同期・通信に使うこともできます。
C#/Javaのvolatileフィールドに対するインクリメント・デクリメントなどはアトミック操作にならないので、当然そういった用途にはInterlockedクラスなどを使うべきですが、単純な代入と参照による状態管理用途であればvolatileでもOKな場面もあります。運用制限を設けて賢く使いましょう。
*1:うわさによると、VC7.1 (VC++ .NET 2003) では、volatile関連で /Oa というさらにぶっ飛んだ最適化オプションがあったそうですが、このオプションはVC8.0 (VC++2005) で削除されたそうです。Visual C++ 2003 ~ 2015 の新機能 | Microsoft Docs