前回は例外のスローに起因するデッドロックを紹介しました。これについては 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 を入れ子にしないことです。