CON02-C. volatile を同期用プリミティブとして使用しない
C 言語規格 [ISO/IEC 9899:2011] セクション 5.1.2.3 第 2 段落には次のように記載されている。
ボラタイルオブジェクトへのアクセス、オブジェクトの変更、ファイルの変更、又はこれらのいずれかの操作を行う関数の呼出しは、すべて副作用(side effect)と呼び、実行環境の状態に変化を生じる。式の評価は、通常、値の計算と副作用の開始を含む。左辺値式の値計算は、指定されているオブジェクトの同一性の判定を含む。
要するに、volatile キーワードの唯一の役割は、この変数はコンパイラが決定できない方法で変更される可能性があること、そのため、volatile としてとして示されたメモリ領域で最適化を実行すべきではないということをコンパイラに伝えることである。たとえば、コンパイラは値をレジスタに格納すべきではないし、高価な直接メモリアクセスを省略するためにレジスタを使用すべきではない。この概念はマルチスレッド処理と緊密に関連している。というのは、共有した変数がキャッシュされると、あるスレッドがその値を更新しても、別のスレッドからは必然的に古いままのデータが見える可能性があるためである。
volatile キーワードのこの特性が、マルチスレッドプログラムのスレッド間で共有した変数のアトミック性を確保するかのような印象を与え、時として誤解を招いている。volatile 宣言された変数はレジスタにキャッシュされないという点が、同期化プリミティブとして安全に使用できるという誤解につながっているのである。実際、ある変数が volatile として宣言された場合、メモリ領域に対する読み取り/書き込み操作の実行順序がコンパイラによって並べ替えられることはないが、それ以外のメモリ領域に対する読み取り/書き込み操作の順序は変更される可能性がある。並べ替えは、同期用の変数に対して非アトミックな操作を実行することになり、エラーの原因になる可能性がある。volatile 修飾子がマルチスレッドプログラムに必要とされる以下の特性のいずれかを備えていると考えてはならない。
- アトミック性: 分割不可能な 1 単位の処理としてメモリ操作を実行できる。
- 可視性: あるスレッドで実行した書き込み操作の結果が別のスレッドから見える。
- 逐次性: あるスレッドによる一連のメモリ操作は、他のスレッドでも同じ順番で見えることが保証される。
これらのどの特性についても、volatile 修飾子によって実現されるという保証はまったくない。こうした特性は仕様に定義されておらず、各種プラットフォーム上でこうした特性を備えた実装方法が採用されているわけでもない。volatile の実装の詳細については、「DCL17-C. 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 を使用するが、今度は 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 flag は、同期用プリミティブが正しく機能するために必要なアトミック性を保証しない。volatile キーワードによる修飾は、同期用プリミティブが満たすべき特性を与えるわけではない。
適合コード
次のコードは、クリティカルセクションを保護するためにミューテックスを使用している。
#include <threads.h>
int account_balance;
mtx_t flag;
/* flag を初期化 */
void debit(unsigned int amount) {
mtx_lock(&flag);
account_balance -= amount; /* クリティカルセクション内 */
mtx_unlock(&flag);
}
リスク評価
|
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
|---|---|---|---|---|---|
|
CON02-C |
中 |
中 |
中 |
P8 |
L2 |
関連するガイドライン
| CERT C++ Secure Coding Standard | CON01-CPP. Do not use volatile as a synchronization primitive |
参考資料
| [ISO/IEC 9899:2011] | Section 5.1.2.3, "Program Execution" |
| [Open Group 2004] | Section 4.11, "Memory Synchronization" |
翻訳元
これは以下のページを翻訳したものです。
CON02-C. Do not use volatile as a synchronization primitive (revision 28)
