このチュートリアルでは、並列スタック ウィンドウの [スレッド] ビューを使用して、C# マルチスレッド アプリケーションをデバッグする方法について説明します。 このウィンドウは、マルチスレッド コードの実行時の動作を理解して確認するのに役立ちます。
スレッド ビューは C++ と Visual Basic でもサポートされているため、C# のこの記事で説明するのと同じ原則が C++ と Visual Basic にも適用されます。
[スレッド] ビューを使用すると、次のことが行えます。
複数のスレッドの呼び出し履歴の視覚化を表示します。これは、現在のスレッドの呼び出し履歴のみを表示する [呼び出し履歴] ウィンドウよりもアプリの状態の全体像を示します。
ブロックされたスレッドやデッドロックされたスレッドなどの問題を特定するのに役立ちます。
C# サンプル
このチュートリアルのサンプル コードは、ゴリラの生活の中で 1 日をシミュレートするアプリケーションを対象としています。 演習の目的は、[並列スタック] ウィンドウの [スレッド] ビューを使用してマルチスレッド アプリケーションをデバッグする方法を理解することです。
このサンプルには、2 つのスレッドが互いに待機しているときに発生するデッドロックの例が含まれています。
呼び出し履歴を直感的にするために、サンプル アプリは次の順番の手順を実行します。
- ゴリラを表すオブジェクトを作成します。
- ゴリラが目を覚ます。
- ゴリラは朝の散歩に行きます。
- ゴリラはジャングルでバナナを見つけます。
- ゴリラは食べる。
- ゴリラはモンキービジネスに従事しています。
マルチスレッドコールスタック
呼び出し履歴の同じセクションがグループ化され、複雑なアプリの視覚化が簡略化されます。
次の概念アニメーションは、グループ化が呼び出し履歴にどのように適用されるかを示しています。 呼び出し履歴の同一セグメントのみがグループ化されます。
サンプル プロジェクトを作成する
プロジェクトを作成するには:
Visual Studio を開き、新しいプロジェクトを作成します。
スタート ウィンドウが開いていない場合は、[ファイル]
[スタート ウィンドウ] 選択します。 [スタート] ウィンドウで、[ 新しいプロジェクト] を選択します。
[ 新しいプロジェクトの作成 ] ウィンドウで、検索ボックスに コンソール と入力します。 次に、[言語] の一覧から [C# ] を選択し、[プラットフォーム] の一覧から [Windows ] を選択します。
言語フィルターとプラットフォーム フィルターを適用した後、.NET 用 コンソール アプリ を選択し、[ 次へ] を選択します。
Note
正しいテンプレートが表示されない場合は、 ツール>Get Tools and Features...に移動し、Visual Studio インストーラーを開きます。 [.NET デスクトップ開発] ワークロードを選択し、[変更] を選びます。
[ 新しいプロジェクトの構成 ] ウィンドウで、名前を入力するか、[ プロジェクト名 ] ボックスに既定の名前を使用します。 その後、 [次へ] を選択します。
.NET の場合は、推奨されるターゲット フレームワークまたは .NET 8 を選択し、[ 作成] を選択します。
新しいコンソール プロジェクトが表示されます。 プロジェクトが作成されると、ソース ファイルが表示されます。
プロジェクトで .cs コード ファイルを開きます。 その内容を削除して、空のコード ファイルを作成します。
選択した言語の次のコードを空のコード ファイルに貼り付けます。
using System.Diagnostics; namespace Multithreaded_Deadlock { class Jungle { public static readonly object tree = new object(); public static readonly object banana_bunch = new object(); public static Barrier barrier = new Barrier(2); public static int FindBananas() { // Lock tree first, then banana lock (tree) { lock (banana_bunch) { Console.WriteLine("Got bananas."); return 0; } } } static void Gorilla_Start(object lockOrderObj) { Debugger.Break(); bool lockTreeFirst = (bool)lockOrderObj; Gorilla koko = new Gorilla(lockTreeFirst); int result = 0; var done = new ManualResetEventSlim(false); Thread t = new Thread(() => { result = koko.WakeUp(); done.Set(); }); t.Start(); done.Wait(); } static void Main(string[] args) { List<Thread> threads = new List<Thread>(); // Start two threads with opposite lock orders threads.Add(new Thread(Gorilla_Start)); threads[0].Start(true); // First gorilla locks tree then banana threads.Add(new Thread(Gorilla_Start)); threads[1].Start(false); // Second gorilla locks banana then tree foreach (var t in threads) { t.Join(); } } } class Gorilla { private readonly bool lockTreeFirst; public Gorilla(bool lockTreeFirst) { this.lockTreeFirst = lockTreeFirst; } public int WakeUp() { int myResult = MorningWalk(); return myResult; } public int MorningWalk() { Debugger.Break(); if (lockTreeFirst) { lock (Jungle.tree) { Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample Jungle.FindBananas(); GobbleUpBananas(); } } else { lock (Jungle.banana_bunch) { Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample Jungle.FindBananas(); GobbleUpBananas(); } } return 0; } public void GobbleUpBananas() { Console.WriteLine("Trying to gobble up food..."); DoSomeMonkeyBusiness(); } public void DoSomeMonkeyBusiness() { Thread.Sleep(1000); Console.WriteLine("Monkey business done"); } } }
コード ファイルを更新したら、変更を保存してソリューションをビルドします。
[ファイル] メニューの [すべてを保存] をクリックします。
[ビルド] メニューの [ソリューションのビルド] を選択します。
[並列スタック] ウィンドウの [スレッド] ビューを使用する
デバッグを開始するには:
[ デバッグ ] メニューの [ デバッグの開始 ] (または F5) を選択し、最初の
Debugger.Break()
がヒットするまで待ちます。F5 キーを 1 回押すと、デバッガーは同じ
Debugger.Break()
行でもう一度一時停止します。これは、2 番目のスレッド内で発生する
Gorilla_Start
の 2 番目の呼び出しで一時停止します。Tip
デバッガーは、スレッドごとにコードに分割されます。 たとえば、 F5 キーを押して実行を続行し、アプリが次のブレークポイントにヒットすると、別のスレッドのコードに分割される可能性があります。 デバッグ目的でこの動作を管理する必要がある場合は、ブレークポイント、条件付きブレークポイントを追加したり、 すべて中断を使用したりできます。 条件付きブレークポイントの使用の詳細については、「条件付きブレークポイント を使用した 1 つのスレッドのフォロー」を参照してください。
[デバッグ > Windows >並列スタック] を選択して [並列スタック] ウィンドウを開き、ウィンドウの [表示] ドロップダウンから [スレッド] を選択します。
スレッド ビューでは、現在のスレッドのスタック フレームと呼び出しパスが青色で強調表示されます。 スレッドの現在の位置が黄色の矢印で表示されます。
Gorilla_Start
の呼び出し履歴のラベルが 2 スレッドであることに注意してください。 最後に F5 キーを押したとき、別のスレッドを開始しました。 複雑なアプリの簡略化のために、同じ呼び出し履歴が 1 つの視覚的表現にグループ化されます。 これにより、特に多くのスレッドを使用するシナリオで、複雑になる可能性のある情報が簡略化されます。デバッグ中に、外部コードが表示されるかどうかを切り替えることができます。 機能を切り替えるには、[ 外部コードの表示] を選択またはオフにします。 外部コードを表示する場合でも、このチュートリアルを使用できますが、結果が図とは異なる場合があります。
もう一度 F5 キーを押すと、デバッガーは
Debugger.Break()
メソッドのMorningWalk
行で一時停止します。[並列スタック] ウィンドウには、
MorningWalk
メソッド内の現在実行中のスレッドの場所が表示されます。グループ化された呼び出し履歴によって表される 2 つのスレッドに関する情報を取得するには、
MorningWalk
メソッドにカーソルを合わせます。現在のスレッドは、[デバッグ] ツールバーの [スレッド ] の一覧にも表示されます。
[スレッド] リストを使用して、デバッガー コンテキストを別のスレッドに切り替えることができます。 これにより、現在実行中のスレッドは変更されず、デバッガー コンテキストだけが変更されます。
または、[スレッド] ビューでメソッドをダブルクリックするか、[スレッド] ビューでメソッドを右クリックし、[ フレームに切り替え>[スレッド ID] を選択して、デバッガー コンテキストを切り替えることもできます。
もう一度 F5 キーを押すと、2 番目のスレッドの
MorningWalk
メソッドでデバッガーが一時停止します。スレッド実行のタイミングに応じて、この時点で個別の呼び出し履歴またはグループ化された呼び出し履歴が表示されます。
上の図では、2 つのスレッドの呼び出し履歴が部分的にグループ化されています。 呼び出し履歴の同一のセグメントがグループ化され、矢印線は分離されたセグメントを指します (つまり、同じではありません)。 現在のスタック フレームは、青色の強調表示で示されます。
もう一度 F5 キーを押すと、長い遅延が発生し、[スレッド] ビューに呼び出し履歴情報が表示されません。
遅延はデッドロックによって発生します。 スレッドがブロックされている場合でも、デバッガーで現在一時停止していないため、スレッド ビューには何も表示されません。
Tip
デッドロックが発生した場合、またはすべてのスレッドが現在ブロックされている場合は、[ すべて中断 ] ボタンを使用して呼び出し履歴情報を取得できます。
デバッグ ツール バーの IDE の上部にある [ すべて中断 ] ボタン (一時停止アイコン) を選択するか、 Ctrl + Alt + Break キーを使用します。
スレッド ビューの呼び出し履歴の上部には、
FindBananas
がデッドロックしていることを示しています。FindBananas
の実行ポインターは、現在のデバッガー コンテキストを示す緑色の丸い矢印ですが、スレッドが現在実行されていないことも示します。コード エディターで、
lock
関数に緑色のカール矢印が表示されます。 2 つのスレッドは、lock
メソッドのFindBananas
関数でブロックされます。スレッドの実行順序に応じて、デッドロックは
lock(tree)
またはlock(banana_bunch)
ステートメントに表示されます。lock
を呼び出すと、FindBananas
メソッド内のスレッドがブロックされます。 一方のスレッドは、tree
のロックがもう一方のスレッドによって解放されるのを待っていますが、もう一方のスレッドは、banana_bunch
のロックを解放する前に、tree
のロックが解除されるのを待ちます。 これは、2 つのスレッドが互いに待機しているときに発生するクラシック デッドロックの例です。Copilot を使用している場合は、AI によって生成されたスレッドの概要を取得して、潜在的なデッドロックを特定することもできます。
サンプル コードを修正する
このコードを修正するには、常にすべてのスレッドで一貫性のあるグローバルな順序で複数のロックを取得します。 これにより、循環待機が回避され、デッドロックが排除されます。
デッドロックを修正するには、
MorningWalk
のコードを次のコードに置き換えます。public int MorningWalk() { Debugger.Break(); // Always lock tree first, then banana_bunch lock (Jungle.tree) { Jungle.barrier.SignalAndWait(5000); // OK to remove lock (Jungle.banana_bunch) { Jungle.FindBananas(); GobbleUpBananas(); } } return 0; }
アプリを再起動します。
Summary
このチュートリアルでは、 並列スタック デバッガー ウィンドウについて説明しました。 マルチスレッド コードを使用する実際のプロジェクトでは、このウィンドウを使用します。 C++、C#、または Visual Basic で記述された並列コードを調べることができます。