次のプログラムを実行すると、コンソール画面に 1 が並んでいったあとで、確実にデッドロックが発生します。なぜでしょうか。
Imports System.Threading Module Module1 Private Class ValuePair Public a As Integer = 2000000000 Public b As Integer = a + 1 End Class Private theVal As ValuePair = New ValuePair Private Sub Increment() Do Until theVal.a = [Integer].MaxValue Monitor.Enter(theVal) With theVal .a += 1 .b = .a + 1 End With Monitor.Exit(theVal) Loop End Sub Public Sub Main() Dim worker As New Thread(AddressOf Increment) worker.Start() Do While worker.IsAlive Monitor.Enter(theVal) With theVal Console.WriteLine(.b - .a) End With Monitor.Exit(theVal) Loop End Sub End Module
問題は theVal のロックを取得するための Enter を呼び出したあとで、Exit が呼び出されない場合があるということです。一見しただけでは気付きにくいのですが、Increment メソッドの中で .b = .a + 1 と書いてある個所。そこでオーバーフローによる例外が発生します。すると、Increment を実行しているスレッドは途端に終了してしまいます。ここで、theVal のロックが、相変わらずこのスレッドによって取得されたままになっていることに注意が必要です。そして今後解放されることは決してありませんので、その解放を待っているメイン スレッドはデッドロックに陥ります。
一度オブジェクトのロックを取得したら、それを何が何でも解放しなければなりません。たとえ例外が発生しようとも、です。そのためには、次のように例外ブロックを使用するとよいでしょう(そもそも例外が起こらないように修正すればよいのですが、ここでは敢えてそのままにしておきます)。
Private Sub Increment() Do Until theVal.a = [Integer].MaxValue Monitor.Enter(theVal) Try With theVal .a += 1 .b = .a + 1 End With Finally Monitor.Exit(theVal) End Try Loop End Sub Public Sub Main() Dim worker As New Thread(AddressOf Increment) worker.Start() Do While worker.IsAlive Monitor.Enter(theVal) Try With theVal Console.WriteLine(.b - .a) End With Finally Monitor.Exit(theVal) End Try Loop End Sub
VB ならば SyncLock ステートメントを使うことで、ほぼ同様のことが実現できます。また、ミューテックスを使えば、万一ロックの解放を忘れてもスレッドが終了した時点で解放されます(この目的での使用は推奨できませんが)。さらに、Interlocked クラスのメソッドを使うという手もあります。詳しくはリファレンス等を参照して下さい。