次のプログラムを実行すると、コンソール画面に 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 クラスのメソッドを使うという手もあります。詳しくはリファレンス等を参照して下さい。