POS03-C. volatile を同期用プリミティブとして使用しない
volatile 修飾子がマルチスレッドプログラムに必要とされる以下の特性を備えている、というのは誤解である。
- アトミック性: 分割不可能な 1 単位の処理としてメモリ操作を実行できる
- 可視性: あるスレッドで実行した書き込み操作の結果が別のスレッドから見える
- 逐次性: あるスレッドによる一連のメモリ操作は、他のスレッドでも同じ順番で見えることが保証される
残念ながら、これらのどの特性についても、volatile 修飾子によって実現されるという保証はまったくない。こうした特性は仕様に定義されておらず、各種プラットフォーム上でこうした特性を備えた実装方法が採用されているわけでもない。volatile の実装の詳細については、「DCL17-C. volatile 修飾された変数が間違ってコンパイルされることに注意」を参照のこと。
C99 標準のセクション 5.1.2.3 第 2 段落には次のように記載されている。
ボラタイルオブジェクトへのアクセス、オブジェクトの変更、ファイルの変更、又はこれらのいずれかの操作を行う関数の呼出しは、すべて副作用(side effect)と呼び、実行環境の状態に変化を生じる。式の評価は、副作用を引き起こしてもよい。副作用完了点(sequence point)と呼ばれる実行順序における特定の点において、それ以前の評価に伴う副作用は、すべて完了していなければならず、それ以降の評価に伴う副作用が既に発生していてはならない。
要するに、volatile キーワードの唯一の役割は、volatile として示されたメモリ領域で最適化を実行してはならない、ということをコンパイラに伝えることである。すなわち、この変数はコンパイラが決定できない方法で変更される可能性があるため、高価なメモリアクセスを省略するために変数をレジスタに格納するという最適化をしてはいけない。この概念はマルチスレッド処理と大いに関係がある。というのも、共有した変数がキャッシュされると、あるスレッドがその値を更新しても、別のスレッドからは古いままのデータが見える可能性があるためである。
volatile キーワードのこの特性が、マルチスレッドプログラムのスレッド間で共有した変数のアトミック性を確保するかのような印象を与え、時として混乱を招いている。volatile 宣言された変数はレジスタにキャッシュされないという点が、同期化プリミティブとして安全に使用できるという思い込みにつながっているのである。実際、volatile 宣言されたメモリ領域に対する読み取り/書き込み操作の実行順序がコンパイラによって並べ替えられることはないが、それ以外のメモリ領域に対する読み取り/書き込み操作の順序は変更される可能性がある。このため、同期用の変数に対して非アトミックな操作を実行することは、エラーの原因になる可能性がある。
違反コード
次のコード例では、同期用プリミティブとして flag を使用している。
bool flag = false; void test() { while (!flag) { sleep(1000); } } void wakeup(){ flag = true; } void debit(unsigned int amount){ test(); account_balance -= amount; }
この例では、クリティカルセクションを実行してよいかどうかの判定に flag の値を使用している。flag 変数は、volatile 宣言されていないため、レジスタにキャッシュされる可能性がある。レジスタ内の値がメモリに書き込まれる前に、別のスレッドの実行がスケジューリングされれば、そのスレッドによる読み取り結果は古いデータのままである可能性がある。
違反コード
以下のコード例は、同期用プリミティブとして flag を使用するが、今度はこれを volatile で修飾している。
volatile bool flag = false; void test() { while (!flag){ sleep(1000); } } void wakeup(){ flag = true; } void debit(unsigned int amount) { test(); account_balance -= amount; }
flag を volatile 宣言したため、古いデータが読み取られる問題は解決している。しかし、同期用プリミティブが正しく機能するために必要なアトミック性はこれでも保証されない。volatile キーワードによる修飾は、同期用プリミティブが満たすべき特性を与えるわけではない。
適合コード
次のコードは、クリティカルセクションを保護するためにミューテックスを使用している。
#include <pthread.h> int account_balance; pthread_mutex_t flag = PTHREAD_MUTEX_INITIALIZER; void debit(unsigned int amount) { pthread_mutex_lock(&flag); account_balance -= amount; /* クリティカルセクション内 */ pthread_mutex_unlock(&flag); }
リスク評価
レコメンデーション | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
POS03-C | 中 | 中 | 中 | P12 | L1 |
参考情報
- [ISO/IEC 9899:1999] Section 5.1.2.3 "Program Execution"
- [Open Group 04] Section 4.11 "Memory Synchronization"
翻訳元
これは以下のページを翻訳したものです。