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

前回は例外のスローに起因するデッドロックを紹介しました。これについては SyncLock ステートメントや Try-Catch-Finally 構文を利用することで回避できることも説明しました。

今回もコード例を先に示すことにします。次のコードのどこでデッドロックが起こるでしょうか。

Imports System.Threading

Module Module1

    Private objA As Object = New Object
    Private objB As Object = New Object

    Sub ThreadProc()

        For i As Integer = 0 To 100
            SyncLock objB
                SyncLock objA
                    Console.WriteLine(i)
                End SyncLock
            End SyncLock
        Next i

    End Sub

    Sub Main()

        Dim theThread As Thread = New Thread(AddressOf ThreadProc)
        theThread.Start()

        For i As Integer = 0 To 100
            SyncLock objA
                SyncLock objB
                    Console.WriteLine(i)
                End SyncLock
            End SyncLock
        Next i

        theThread.Join()

    End Sub
End Module

この場合、両方のメソッドの、内側の SyncLock で停止します。具体的には次のようになります。

[Main]        [ThreadProc]
SyncLock objA               (1)
              SyncLock objB (2)
SyncLock objB               (3)
wait...
              SyncLock objA (4)
              wait...

ただし、タイミング次第では次のように実行されることもあるため、必ずしもデッドロックが起こるとは限りません。

[Main]        [ThreadProc]
SyncLock objA               (1)
SyncLock objB               (3)
              SyncLock objB (2)
              wait ...
End SyncLockB
              SyncLock objA (4)
              wait ...
End SyncLockA
              End SyncLockB
              End SyncLockA

エラーが常に起こる場合は、明らかに、どこかに根本的な問題があります。しかし常には起こらない場合、問題がどこにあるのか特定するのが困難です。おそらく単体では何の問題もないからです。まぁマルチスレッドなプログラムにおいては、このような再現性のないエラーがしばしば起こるものですが、修正できればそれに越したことはありません。このコードの場合は次のようにします。

Imports System.Threading

Module Module1

    Private objA As Object = New Object
    Private objB As Object = New Object

    Sub ThreadProc()

        For i As Integer = 0 To 100
            SyncLock objA '変更した
                SyncLock objB '変更した
                    Console.WriteLine(i)
                End SyncLock
            End SyncLock
        Next i

    End Sub

    Sub Main()

        Dim theThread As Thread = New Thread(AddressOf ThreadProc)
        theThread.Start()

        For i As Integer = 0 To 100
            SyncLock objA
                SyncLock objB
                    Console.WriteLine(i)
                End SyncLock
            End SyncLock
        Next i

        theThread.Join()

    End Sub
End Module

単にロック取得の順序を変えただけですが、こうすれば常に次のような順序で実行されます。

[Main]        [ThreadProc]
SyncLock objA
              SyncLock objA
              wait ...
SyncLock objB
End SyncLockB
End SyncLockA
              SyncLock objB
              End SyncLockB
              End SyncLockA

つまり、複数のオブジェクトのロックを取得する必要がある場合は、すべてのスレッドで同じ順序で取得するとよいというわけです。もっとも、この場合は objB のロックを取得する必要はありませんが。

今回はデッドロックのパターンを示すのが目的なので、極力単純なプログラムにしてみましたが、実際にはもっと複雑な状況が予想されます。たとえばソース ファイルが複数だとか、プログラマが複数だとか、あるいは見えないところ(参照しているアセンブリなど)でロックを取得しているといったことです。回避する最善の方法は、同期せずに済む方法を探すことです。次善策は、SyncLock を入れ子にしないことです。