マルチスレッドのプログラミング

デッドロックの回避

ある一組のスレッドが一連の資源の獲得で競合したまま、永久にブロックされた状態に陥っているとき、その状態をデッドロックと呼びます。実行可能なスレッドがあるからといって、デッドロックが発生していないという証拠にはなりません。

代表的なデッドロックは、「自己デッドロック」です (「再帰的なデッドロック」とも言います)。自己デッドロックは、すでに保持しているロックをスレッドがもう一度獲得しようとしたとき発生します。これは、ちょっとしたミスで簡単に発生してしまいます。

たとえば、一連のモジュール関数で構成されるコードモニタを考えます。各モジュール関数が実行中に保持する相互排他ロックがどれも同じであると、このモジュール内の相互排他ロックの保護下にある関数間で呼び出しが行われた場合に、たちまちデッドロックが発生します。また、ある関数がこのモジュールの外部のコードを呼び出し、そこから再び同じ相互排他ロックで保護されているモジュールを呼び出した場合にもデッドロックが発生します。

この種のデッドロックを回避するには、モジュールの外部の関数を呼び出す場合、その関数が再び元のモジュールを呼び出さないことを確認できないときは各不変式を再び真にして、すべてのモジュール内のロックを解除してから呼び出すようにします。次に、その呼び出しを終了してもう一度ロックを獲得した後、所定の状態を評価して、意図している操作がまだ有効であるか確認します。

もう 1 つ別の種類のデッドロックがあります。スレッド 1、2 がそれぞれ mutex A、B のロックを獲得しているものと仮定します。次に、スレッド 1 が mutex B を、スレッド 2 が mutex A をロックしようとすると、スレッド 1 は mutex B を待ったままブロックされ、スレッド 2 は mutex A を待ったままブロックされます。結局、両方のスレッドは身動きがとれなくなって永久にブロックされ、デッドロック状態となります。

この種のデッドロックを回避するには、ロックを行う順序を一定に保ちます。この方法を「ロック階層」と呼びます。すべてのスレッドが常に一定の順序でロックを行う限り、この種のデッドロックは生じません。

しかし、ロックを行う順序を一定に保つという規則を守っていればよいとは必ずしも言えません。たとえば、スレッド 2 が mutex B を保持している間に、モジュールの状態に関して数多くの仮定条件を立てた場合、次に mutex A のロックを獲得するために mutex B のロックを解除し、規則通り mutex A のロックを獲得した後にもう一度 mutex B のロックを獲得しても先の仮定は無駄になり、モジュールの状態をもう一度評価しなければならなくなります。

通常、ブロックを行う同期プリミティブには、ロックを獲得しようとしてできなかった場合にエラーとなる類似のプリミティブ (たとえば、mutex_trylock()) が用意されています。これを使うと、競合がなければロック階層を守らないという方法でロックを実行できます。競合があるときは、通常は保持しているロックをいったん解除してから、順番にロックを実行しなければなりません。

デッドロックのスケジューリング

マルチスレッドプログラムでは、ロックが獲得される順序がシステム的に不定であることが原因で、特定のスレッドがロック (通常は条件変数) を獲得できるように見えても、実際にはロックを獲得できないという問題があります。

通常、この問題は次のような状況で起こります。スレッドが、保持していたロックを解除し、少し時間をおいてからもう一度ロックを獲得するものとします。このとき、ロックはいったん解除されたので、他のスレッドがロックを獲得したと考えがちです。しかし、ロックを保持していたスレッドは、ブロックされなければロック解除後も引き続き実行され、もう一度ロックを獲得するので、結局その間に他のスレッドは実行されません。

通常、この種の問題を解決するには、もう一度ロックを獲得する前に thr_yield(3T) を呼び出します。これで他のスレッドが実行され、そのスレッドはロックを獲得できるようになります。

必要なタイムスライスの大きさはアプリケーションに依存するため、スレッドライブラリでは特に制限していません。thr_yield() を呼び出して、スレッドが共有する時間を設定してください。

ロックに関する指針

次に、ロックのための簡単な指針を示します。