デッドロック・パターン 其の壱

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