意図的にライブロック起こすことの難しさについて考えてみる
今回挑戦したのは、2つのスレッドプログラムが相手に合わせてミューテックス変数のロックを譲り合う処理を繰り返すことで発生するライブロック現象でした。
ライブロックを起こすために時間をかけて調整をした結果、現象を起こすことができたように見えますが、長時間動かした場合には少しずつ処理が進むことも考えられます。
ライブロックしたプログラム(2つのスレッド関数)
ライブロックの現象は以下の通りです。
- ループしながら、確保したい2つのミューテックス変数のロック順を入れ替えながら2変数をロックしようと試みる
- ロック順を変えながらも2変数をロックすることができず、ループから抜けられない両スレッド関数は先へ進めない
ロックしたいミューテックス変数を常に相手のスレッドがロックしている時にBUSYとなるが、printfを有効にしたり、両スレッドのusleepを同時に有効にするとライブロック状態から抜けて問題なく動作しました。
2つのスレッドが全く同じ構造のループで発生すると考えていたライブロックですが、実際に発生したライブロックではループの構造が同じではありませんでした。
プログラムは見た目ほど簡単な構造ではない
話が並列処理と離れますが、ソースプログラムがコンパイルされて動作する時には見かけ以上にいろんな処理を行っています。上記プログラムのループ部分を見ても、実はマクロが書かれていたり関数呼び出しがあります。
ループ処理部分
関数呼び出し処理を例に挙げると、そこでは以下のような一連の処理が行われます。
- 子関数呼び出しのための引数情報の設定
- 子関数呼び出しの際に壊れてしまう情報の退避(親関数担当分)
- 子関数が壊す情報を退避(子関数担当分)
- 子関数の処理
- 子関数が壊した情報の復元
- 親関数へのリターン
- 子関数呼び出しの前に退避した情報の復元
usleep()であれば一時的な処理の停止を行うために、printf()であれば文字列生成のために、また別の関数を呼び出します。
参考図:関数呼び出しとスタックメモリの関係
上記の処理を行うために、スタックメモリを使用して関数呼び出しが行われる。
親・子・孫 の色分け関数が使用するスタックメモリを同じ色のメモリ図内に示している。
そして、動作しているのは2つのスレッドだけではありません。
Windowsのタスクマネージャで見られるように多くのプロセスが動作しており、それぞれの処理は止められたり、動いたりしながら処理を行っています。
このような状況では、ちょっとしたタイミングのズレによってループの1回転の時間にばらつきが発生して、ライブロックがロック状態から抜けることが考えられます。
狙って起こそうとしたライブロックは、簡単な構造であっても発生させるのは簡単ではありませんでした。
もし、実際のシステムでライブロックが発生したとしたら、その確認・再現・デバッグはとても難しいものになりそうです。
次回は、それらについて考えてみます。
コメントをお書きください