導入:なぜ「volatile」の誤解が危険なのか
C++を学習していると、マルチスレッドプログラミングにおいて「スレッド間で値を共有するために volatile を使う」という古い情報や誤解に出会うことがあります。しかし、volatile はスレッド間の同期には全く役に立ちません。 それどころか、マルチスレッド環境で volatile を使用すると、プログラムが予期せぬ動作を引き起こす「データレース」という深刻なバグの原因になります。この記事では、なぜ volatile がスレッド同期に使えないのか、そして代わりに何を使うべきかを解説します。
基礎知識:volatile の本来の役割
C++における volatile とは、コンパイラに対して「この変数は、プログラムの外部(ハードウェアなど)から値が勝手に書き換わる可能性があるため、最適化をしないでくれ」と伝えるキーワードです。
具体的には、コンパイラが「この変数の値はさっき読み込んだものと同じだから、わざわざメモリを見に行かずレジスタに保存しておいた値を使おう」といった最適化を禁止します。主に、組み込み開発におけるメモリマップドI/O(MMIO)などのハードウェア制御で使われるものです。決して、マルチスレッドにおける「スレッド間の合図」を目的とした機能ではありません。
実装:なぜ volatile では不十分なのか
現代のCPUは、処理速度を上げるためにアウトオブオーダー実行という「命令の実行順序を勝手に入れ替える」機能を持っています。volatile はコンパイラの最適化を止めることはできますが、CPU自体の命令の並べ替え(メモリバリア)を止めることはできません。
そのため、スレッドAで volatile 変数を更新しても、スレッドBからは書き換えの順序が異なって見えてしまうことがあり、同期が正しく行われません。これを解決するために、C++11以降では std::atomic を使用することが標準となっています。
サンプルプログラム:std::atomic を使った正しい実装
以下のサンプルコードは、スレッド間でのフラグ通信を安全に行うための正しい実装例です。
include
include
// volatile bool ready = false; // これはNG!データレースが発生する
std::atomic
void worker_thread() {
// ready が true になるまで待機する
while (!ready.load()) {
// ここでの読み取りは安全に行われる
}
std::cout << "スレッド: 処理を開始します!" << std::endl;
}
int main() {
std::thread t(worker_thread);
// メインスレッドで準備を行う
std::cout << "メイン: 準備中..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
// フラグを立ててスレッドに合図を送る
ready.store(true);
t.join();
return 0;
}
応用・注意点:現場での使い分け
現場の開発において、以下のルールを守ることでバグを未然に防ぐことができます。
1. スレッド間の通信には必ず std::atomic を使う: メモリの可視性や命令の順序を保証してくれるため、安全にデータを共有できます。
2. volatile はハードウェア制御のみ: センサーからの読み取りや、シグナルハンドラとの連携など、厳密に「外部から値が変わる」状況のみに使用してください。
3. データレースは未定義動作(UB): volatile を使ったマルチスレッド通信は「未定義動作」を引き起こします。プログラムが突然クラッシュしたり、無限ループに陥ったりする原因になるため、絶対に避けてください。
これからは「マルチスレッドなら atomic」と覚えておきましょう。C++の設計意図を正しく理解することが、堅牢なプログラムを書くための第一歩です。

コメント