オペレーティングシステム演習 排他制御のトラブル

助教時代にオペレーティングシステム演習を担当していた。実際は、准教授が教材を作っていて、演習時のサポートをしていただけであった。今回は排他制御の課題でトラブルがあった話である。

並行プログラミングで共有変数へのアクセスが課題であった。2つのスレッドが共有変数(整数)をインクリメントするプログラムだった。まず、排他制御なしのプログラムを実行させ、期待通りの結果にならないことを確認させた。次に排他制御を実装したプログラムを実行させ、問題が解決することを確認させた。一人の学生が排他制御できていないと主張した。准教授の与えたプログラムを実行しただけなので、学生のプログラミングミスではない。准教授は何か思い当たることがないか聞いた。学生も当初は何も思いつかなかったが、後になって alias で最適化マックスでコンパイルしていることに気付いた。准教授は最適化で問題が起こるとは考えにくいと直ぐに理由は判明しなかった。

下のプログラム断片を見ていただきたい。元々は inc_x() 内で排他制御をする課題だった。これは後に演習を私が引き継いで、今回の話題となったトラブルを別の課題としたプログラム断片である。2スレッドが並行して sub() を実行する。元々の課題における inc_x() では、局所変数 y を定義し、 y=x; 空ループ; x=y+1; である。空ループ実行中に他スレッドから x にアクセスがあると問題が生じる。このプログラムでは、共有変数の読み込みから書き込みまでの時間が短いため、問題が顕在化しない。しかし、最適化すると問題が顕在化する。なぜだろうか。

int x = 0; //大域変数、共有変数

void inc_x()
{
x++; // 本来なら排他制御が必要だが、共有変数へのアクセス時間が短く、影響はなかった
return;
}

void * sub( void * t )
{
for( int i = 0 ; i < 1000000000 ; i++ )
inc_x();

return NULL;
}

まず、アーキテクチャレベルで動作を考えよう。変数 x はメモリ上に確保されている。変数 x の値をメモリからレジスタに読み込み、インクリメント、レジスタの値をメモリに戻す、といった手順で実行される。問題が顕在化しなかったのは、読み込みから書き込みまでの時間が短かったから。最適化すると何が起こるのか。inc_x() を2回実行すると、
  読み込み、インクリメント、書き込み、読み込み、インクリメント、書き込み
となる。シングルスレッドでは、中央の書き込みと読み込みの間で x の値に変更はないので、無駄な処理とみなされる。最適化により省略されて
  読み込み、インクリメント、インクリメント、書き込み
となることが想定される。これが繰り返されれば、x の値はレジスタに保持され続けることになり、その間に他スレッドからアクセスがあれば、問題が顕在化する。

部屋を一回りしながら考えて結論に至った。学生に volatile で解決しないかと伝えた。学生は学部3年だったが、それだけで理解する程度に優秀(IT系だけ優秀だったので、成績上位ではない)だった。volatile で指定された変数は最適化を抑制される。こうして無事解決し、私が科目を引き継いだ後に課題に取り入れた。