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