マネージドコードのデッドロックシナリオ (1)
デッドロックでよく取り上げられるのは、2つのスレッドが互いにロックを取得しようとして、どちらも解放されない状態になるシナリオです。それでは面白くないので、ここでは3つのスレッドが絡むマネージドコードのデッドロックのシナリオを紹介します。
サンプルプログラム
using System;
using System.Threading;
namespace ConsoleApp1
{
internal class Program
{
private static readonly object lock1 = new object();
private static readonly object lock2 = new object();
private static readonly object lock3 = new object();
static void Main()
{
Thread t1 = new Thread(Thread1);
Thread t2 = new Thread(Thread2);
Thread t3 = new Thread(Thread3);
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine("All threads completed execution.");
}
static void Thread1()
{
lock (lock1)
{
Console.WriteLine("Thread 1 acquired lock1");
Thread.Sleep(100); // Simulate work
lock (lock2) ;
Console.WriteLine("Thread 1 acquired lock2");
}
}
static void Thread2()
{
lock (lock2)
{
Console.WriteLine("Thread 2 acquired lock2");
Thread.Sleep(100); // Simulate work
lock (lock3) ;
Console.WriteLine("Thread 2 acquired lock3");
}
}
static void Thread3()
{
lock (lock3)
{
Console.WriteLine("Thread 3 acquired lock3");
Thread.Sleep(100); // Simulate work
lock (lock1) ;
Console.WriteLine("Thread 3 acquired lock1");
}
}
}
}
デッドロックを疑うべき状況
デッドロックが発生しているかどうかを疑うべき一般的な状況は以下の通りです。
- アプリケーションが応答しなくなった場合
- アプリケーションが特定の操作に対して長時間応答しない場合
- ログに「タイムアウト」や「ロック待ち」などのメッセージが記録されている場合
- スレッドの状態が「待機中」や「ブロック中」である場合
負荷が高い処理が実行されている場合にも同様の状況となる場合がありますが、その場合はCPU使用率が高く、スレッドの状態が「実行中」であることが多いです。デッドロックは通常、スレッドが互いにロックを待ち合う状態で発生するため、CPU使用率は低くなることが一般的です。
情報採取
デッドロックが発生した場合、ハングダンプを取得して、どのスレッドがどのロックを保持しているかを確認することが重要です。また、デッドロックと負荷が高い処理を実行している状況の判別が難しい場合があるため、ダンプファイルは1つだけでなく少し時間を空けて複数取得しておくことをお勧めします。これにより、スレッドの状態やロックの状況を比較することができます。ハングダンプは以下に挙げるいくつかの方法で取得することが可能ですが、特に事情がなければprocdump
ツールを使用することをお勧めします。
1. タスクマネージャーを使用してダンプを取得する
タスクマネージャーを使用してダンプファイルを取得する方法は、特に簡単で手軽です。以下の手順でダンプファイルを取得できます。
- タスクマネージャーを開きます。
- 詳細タブを選択します。
- 対象のプロセスを右クリックし、「ダンプファイルの作成」を選択します。
- ダンプファイルの保存先を指定し、ダンプを作成します
タスクマネージャーを使用してダンプファイルを取得する場合の注意点を挙げておきます。
- .NETアプリケーションの場合、プロセスのアーキテクチャがx86かx64かを確認して、適切なタスクマネージャーを使用する必要があります。32ビットアプリケーションは32ビットのタスクマネージャーで、64ビットアプリケーションは64ビットのタスクマネージャーでダンプを取得してください。異なるアーキテクチャのタスクマネージャーを使用すると、マネージドコードの処理について解析することができません。
- Windows 7以前のバージョンでは、タスクマネージャーから取得できるダンプファイルは、プロセスのメモリの内容を含む完全なダンプではなく、スレッドのスタックトレースのみを含む簡易的なダンプとなります。このため、デッドロックの解析には不十分な場合があります。また、この場合、マネージドコードのスタックについては解析することができません。
2. Sysinternalsのprocdump
ツールを使用してダンプを取得する
Sysinternalsのprocdump
ツールを使用してダンプファイルを取得する方法は、より詳細な情報を得ることができるため、デッドロックの解析に適しています。以下の手順でダンプファイルを取得できます。
- Sysinternalsのプロセスダンプツールをダウンロードします。
- コマンドプロンプトを管理者として起動します。
procdump
コマンドを使用して、対象のプロセスのダンプを取得します。以下は基本的なコマンドの例です。ここで、procdump -ma <プロセスID> <ダンプファイルのパス>
-ma
オプションは完全なダンプを取得するためのオプションです。<プロセスID>
は対象のプロセスのID、<ダンプファイルのパス>
はダンプファイルを保存するパスを指定します。- ダンプファイルが作成されると、指定したパスに保存されます。
いくつか具体的なコマンドの例を挙げておきます。
-
procdump -ma -s 5 -n 3 <プロセスID> <ダンプファイルのパス>
指定したプロセスの完全なダンプを取得し、5秒ごとに最大3回までダンプを取得します。
-
procdump -ma -w <プロセス名> <ダンプファイルのパス>
指定したプロセス名のプロセスが起動するまで待機し、起動後に完全なダンプを取得します。プロセス名は、実行ファイルの名前(例:
myapp.exe
)を指定します。 -
procdump -ma -h <プロセスID> <ダンプファイルのパス>
指定したプロセスが「応答なし」となった場合にハングダンプを取得します。「応答なし」はアプリケーションがウィンドウメッセージを一定時間以上処理できない状態が継続したことを示しており、これはデッドロックが疑われる状況の1つです。
3. dotnet dump
コマンドを使用してダンプを取得する
.NET Coreおよび.NETアプリケーションでは、dotnet-dump
コマンドを使用してダンプファイルを取得することができます。以下の手順でダンプファイルを取得できます。
- .NET SDKがインストールされていることを確認します。
- コマンドプロンプトまたはPowerShellを開きます。
dotnet tool install --global dotnet-dump
コマンドを実行して、dotnet-dump
ツールをインストールします。- 対象のプロセスのダンプを取得するために、以下のコマンドを実行します。
ここで、
dotnet-dump collect -p <プロセスID> -o <ダンプファイルのパス>
<プロセスID>
は対象のプロセスのID、<ダンプファイルのパス>
はダンプファイルを保存するパスを指定します。 - ダンプファイルが作成されると、指定したパスに保存されます。
ダンプファイルの解析
1. Visual Studioを使用してダンプを解析する
-
Visual Studioを起動してメニューから
ファイル
>開く
>ファイル
を選択し、取得したダンプファイルを開きます。 -
ダンプファイルの概要が表示されるので、
マネージのみでデバッグ
を選択します。 -
ダンプファイルが読み込まれて自動的にデッドロックが検出されます。
-
検出されている場合はツールチップから
並列スタックの表示
、検出されていない場合はメニューからデバッグ
>ウィンドウ
>並列スタック
を選択します。 -
並列スタックウィンドウが表示され、各スレッドのスタックトレースが表示されます。デッドロックが発生している場合、複数のスレッドが互いにロックを待ち合っていることが確認できます。
-
並列スタックウィンドウの右上に
要約する
と表示されているGitHub Copilotのアイコンをクリックすると、GitHub Copilotチャットが起動し、デッドロックの原因や解決方法についてのアドバイスを受けることができます。GitHub Copilotチャットの出力例アプリケーション全体の状態を見ると、複数のスレッドが相互にロックを待機し合っている典型的なデッドロック状態に陥っています。具体的には、#ParallelStacksNode:6240:'[6240] <名前なし>'、#ParallelStacksNode:10472:'[10472] <名前なし>'、#ParallelStacksNode:13908:'[13908] <名前なし>' の3つのスレッドが、それぞれ他のスレッドが所有するロックの解放を待っており、循環的な依存関係が発生しています。このため、これらのスレッドは進行できず、アプリケーションの一部または全体がハングしている状態です。
一方、#ParallelStacksNode:9704:'[9704] メイン スレッド' は特に問題なくMainメソッドを実行中であり、現時点では異常は見られません。
今後の調査で最も注目すべきは、デッドロックに関与している #ParallelStacksNode:6240:'[6240] <名前なし>'、#ParallelStacksNode:10472:'[10472] <名前なし>'、#ParallelStacksNode:13908:'[13908] <名前なし>' の3スレッドです。これらのスレッドのロック取得順序やリソース管理の実装を詳細に確認する必要があります。
まとめると、アプリケーションはクラッシュしていませんが、明確なハング(デッドロック)状態にあります。 -
GitHub Copilotチャットにお願いすれば図示することもできます。
2. WinDbgを使用してダンプを解析する
-
WinDbgを起動し、メニューから
ファイル
>Start Debugging
>Open dump file
を選択して、取得したダンプファイルを開きます。 -
sos.dllデバッガー拡張をロードします。
.loadby sos clr
-
各マネージドスレッドのスタックを表示します。複数のスレッドが Monitor.Enterで待機している状況が確認できます。
0:008> ~*e!ClrStack
OS Thread Id: 0x261c (0)
Child SP IP Call Site
012ff418 772d9c5c [HelperMethodFrame_1OBJ: 012ff418] System.Threading.Thread.JoinInternal(Int32)
012ff49c 6e28b04e System.Threading.Thread.Join()
012ff4a0 019e0a04 ConsoleApp1.Program.Main() [C:\source\repos\ConsoleApp1\Program.cs @ 22]
012ff63c 6ec52536 [GCFrame: 012ff63c]
...
OS Thread Id: 0x3630 (6)
Child SP IP Call Site
05a8f080 772d9c5c [GCFrame: 05a8f080]
05a8f1b4 772d9c5c [GCFrame: 05a8f1b4]
05a8f160 772d9c5c [HelperMethodFrame_1OBJ: 05a8f160] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
05a8f1f8 6db18c78 System.Threading.Monitor.Enter(System.Object, Boolean ByRef)
05a8f208 019e0ab5 ConsoleApp1.Program.Thread1() [C:\source\repos\ConsoleApp1\Program.cs @ 35]
05a8f238 6dbc3c2d System.Threading.ThreadHelper.ThreadStart_Context(System.Object)
05a8f244 6db18e34 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
05a8f2b0 6db18d67 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
05a8f2c4 6db18d24 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
05a8f2dc 6dbc3b87 System.Threading.ThreadHelper.ThreadStart()
05a8f4b4 6ec52536 [GCFrame: 05a8f4b4]
05a8f5c4 6ec52536 [DebuggerU2MCatchHandlerFrame: 05a8f5c4]
OS Thread Id: 0x148 (7)
Child SP IP Call Site
05c4f280 772d9c5c [GCFrame: 05c4f280]
05c4f3b4 772d9c5c [GCFrame: 05c4f3b4]
05c4f360 772d9c5c [HelperMethodFrame_1OBJ: 05c4f360] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
05c4f3f8 6db18c78 System.Threading.Monitor.Enter(System.Object, Boolean ByRef)
05c4f408 019e0bbd ConsoleApp1.Program.Thread2() [C:\source\repos\ConsoleApp1\Program.cs @ 46]
05c4f438 6dbc3c2d System.Threading.ThreadHelper.ThreadStart_Context(System.Object)
05c4f444 6db18e34 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
05c4f4b0 6db18d67 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
05c4f4c4 6db18d24 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
05c4f4dc 6dbc3b87 System.Threading.ThreadHelper.ThreadStart()
05c4f6b4 6ec52536 [GCFrame: 05c4f6b4]
05c4f7c4 6ec52536 [DebuggerU2MCatchHandlerFrame: 05c4f7c4]
OS Thread Id: 0x56c (8)
Child SP IP Call Site
05d4f4c0 772d9c5c [GCFrame: 05d4f4c0]
05d4f5f4 772d9c5c [GCFrame: 05d4f5f4]
05d4f5a0 772d9c5c [HelperMethodFrame_1OBJ: 05d4f5a0] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
05d4f638 6db18c78 System.Threading.Monitor.Enter(System.Object, Boolean ByRef)
05d4f648 019e0cc5 ConsoleApp1.Program.Thread3() [C:\source\repos\ConsoleApp1\Program.cs @ 57]
05d4f678 6dbc3c2d System.Threading.ThreadHelper.ThreadStart_Context(System.Object)
05d4f684 6db18e34 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
05d4f6f0 6db18d67 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
05d4f704 6db18d24 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
05d4f71c 6dbc3b87 System.Threading.ThreadHelper.ThreadStart()
05d4f8f4 6ec52536 [GCFrame: 05d4f8f4]
05d4fa04 6ec52536 [DebuggerU2MCatchHandlerFrame: 05d4fa04] -
同期オブジェクトの状態を表示します。各スレッドがどのロックを保持しているかを確認できます。
0:008> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
5 0160024c 3 1 015ff848 56c 8 03472308 System.Object
6 01600280 3 1 015fe9d0 3630 6 034722f0 System.Object
7 016002b4 3 1 015fef18 148 7 034722fc System.ObjectOwning thread info
列は先頭から順に、CLR内部のスレッドのデータ構造へのポインタ、OSスレッドID、デバッガスレッドIDが出力されます。SyncBlock Owner
列は先頭から順に、同期オブジェクトのアドレス、同期オブジェクトの型名が出力されます。例えば最初の行は、0x03472308のSystem.Object
型のオブジェクトが、OSスレッドID:0x56c(デバッガースレッドID:8)のスレッドによって所有されていることを示しています。 -
各スレッドのスタック上のオブジェクトの状態を確認します。通常、Monitor.Enterで待機しているスレッドのスタックの最上位には、ロックを獲得しようとしている同期オブジェクトが現れます。
0:008> ~*e!dso
OS Thread Id: 0x261c (0)
ESP/REG Object Name
012FF480 03472384 System.Threading.Thread
012FF4A0 034724b4 System.Threading.Thread
012FF4A4 03472494 System.Threading.ThreadStart
012FF4A8 0347242c System.Threading.Thread
012FF4AC 0347240c System.Threading.ThreadStart
012FF4B0 03472384 System.Threading.Thread
012FF4B4 03472364 System.Threading.ThreadStart
012FF4B8 034724b4 System.Threading.Thread
012FF4BC 0347242c System.Threading.Thread
012FF4C0 03472384 System.Threading.Thread
012FFB0C 04472428 System.Object[] (System.Object[])
012FFC94 03471238 System.SharedStatics
012FFC98 03471238 System.SharedStatics
...
OS Thread Id: 0x3630 (6)
ESP/REG Object Name
05A8EDBC 03479ff4 System.Char[]
05A8F0B4 034722fc System.Object
05A8F100 034723cc System.Threading.ContextCallback
05A8F13C 034722fc System.Object
05A8F150 034722fc System.Object
05A8F158 034723cc System.Threading.ContextCallback
05A8F178 034723b8 System.Threading.ThreadHelper
05A8F17C 034722fc System.Object
05A8F18C 034723cc System.Threading.ContextCallback
05A8F1DC 034722fc System.Object
05A8F1F8 034723b8 System.Threading.ThreadHelper
05A8F208 034722fc System.Object
05A8F20C 034722f0 System.Object
05A8F228 034723b8 System.Threading.ThreadHelper
05A8F238 03472550 System.Threading.ExecutionContext
05A8F244 03472384 System.Threading.Thread
05A8F254 03472384 System.Threading.Thread
05A8F298 03472550 System.Threading.ExecutionContext
05A8F29C 034723cc System.Threading.ContextCallback
05A8F2AC 034723b8 System.Threading.ThreadHelper
05A8F2C0 034723b8 System.Threading.ThreadHelper
05A8F2C8 034723b8 System.Threading.ThreadHelper
05A8F2CC 03472550 System.Threading.ExecutionContext
05A8F2D8 034723b8 System.Threading.ThreadHelper
05A8F354 034723ec System.Threading.ThreadStart
05A8F4DC 034723ec System.Threading.ThreadStart
05A8F4F0 034723ec System.Threading.ThreadStart
OS Thread Id: 0x148 (7)
ESP/REG Object Name
05C4F2B4 03472308 System.Object
05C4F300 034723cc System.Threading.ContextCallback
05C4F33C 03472308 System.Object
05C4F350 03472308 System.Object
05C4F358 034723cc System.Threading.ContextCallback
05C4F37C 03472308 System.Object
05C4F38C 034723cc System.Threading.ContextCallback
05C4F3DC 03472308 System.Object
05C4F3F8 03472460 System.Threading.ThreadHelper
05C4F408 03472308 System.Object
05C4F40C 034722fc System.Object
05C4F428 03472460 System.Threading.ThreadHelper
05C4F438 0347257c System.Threading.ExecutionContext
05C4F444 0347242c System.Threading.Thread
05C4F454 0347242c System.Threading.Thread
05C4F498 0347257c System.Threading.ExecutionContext
05C4F49C 034723cc System.Threading.ContextCallback
05C4F4AC 03472460 System.Threading.ThreadHelper
05C4F4C0 03472460 System.Threading.ThreadHelper
05C4F4C8 03472460 System.Threading.ThreadHelper
05C4F4CC 0347257c System.Threading.ExecutionContext
05C4F4D8 03472460 System.Threading.ThreadHelper
05C4F554 03472474 System.Threading.ThreadStart
05C4F6DC 03472474 System.Threading.ThreadStart
05C4F6F0 03472474 System.Threading.ThreadStart
OS Thread Id: 0x56c (8)
ESP/REG Object Name
05D4F4F4 034722f0 System.Object
05D4F540 034723cc System.Threading.ContextCallback
05D4F57C 034722f0 System.Object
05D4F590 034722f0 System.Object
05D4F598 034723cc System.Threading.ContextCallback
05D4F5B8 034724e8 System.Threading.ThreadHelper
05D4F5BC 034722f0 System.Object
05D4F5CC 034723cc System.Threading.ContextCallback
05D4F61C 034722f0 System.Object
05D4F638 034724e8 System.Threading.ThreadHelper
05D4F648 034722f0 System.Object
05D4F64C 03472308 System.Object
05D4F668 034724e8 System.Threading.ThreadHelper
05D4F678 034725a8 System.Threading.ExecutionContext
05D4F684 034724b4 System.Threading.Thread
05D4F694 034724b4 System.Threading.Thread
05D4F6D8 034725a8 System.Threading.ExecutionContext
05D4F6DC 034723cc System.Threading.ContextCallback
05D4F6EC 034724e8 System.Threading.ThreadHelper
05D4F700 034724e8 System.Threading.ThreadHelper
05D4F708 034724e8 System.Threading.ThreadHelper
05D4F70C 034725a8 System.Threading.ExecutionContext
05D4F718 034724e8 System.Threading.ThreadHelper
05D4F794 034724fc System.Threading.ThreadStart
05D4F91C 034724fc System.Threading.ThreadStart
05D4F930 034724fc System.Threading.ThreadStart -
!syncblkコマンドと!dsoコマンドの結果を照らし合わせて、各スレッドがどのロックを保持しているか、どのロックを待機しているかを確認します。例えば、OSスレッドID:0x56cのスレッドは、0x03472308の
System.Object
型のオブジェクトを保持しており、OSスレッドID:0x148のスレッドはこのオブジェクトのロックを待機しています。表にまとめると以下のようになり、スレッド間で循環的な依存関係が発生していることがわかります。OSスレッドID 保持しているロック 待機しているロック 0x56c 0x03472308 0x034722f0 0x148 0x034722fc 0x03472308 0x3630 0x034722f0 0x034722fc
なお、WinDbgにもVisual Studioと同様に並列スタックウィンドウがあります。ただし、デッドロックの自動的な検出や解析を支援する機能はありません。WinDbgの並列スタックウィンドウを使用するには、メニューからView
>Parallel Stacks
を選択します。WinDbgの並列スタックウィンドウでは、各スレッドのスタックトレースが表示され、スレッド間の依存関係をまとめて視覚的に確認することができます。
デッドロックの解消
デッドロックを解消するためには、以下の方法を検討します。
- ロックの順序を統一する: すべてのスレッドでロックを取得する順序を統一することで、デッドロックの可能性を減らします。
- タイムアウトを設定する: ロック取得時にタイムアウトを設定し、一定時間内にロックが取得できない場合は処理を中断するようにします。
- 非同期処理の利用: 非同期処理を使用して、ロックの競合を減らすことができます。非同期メソッドを使用することで、スレッドのブロッキングを避けることができます。