次の方法で共有


スレッド ビューを使用してデッドロックをデバッグする

このチュートリアルでは、並列スタック ウィンドウの [スレッド] ビューを使用して、C# マルチスレッド アプリケーションをデバッグする方法について説明します。 このウィンドウは、マルチスレッド コードの実行時の動作を理解して確認するのに役立ちます。

スレッド ビューは C++ と Visual Basic でもサポートされているため、C# のこの記事で説明するのと同じ原則が C++ と Visual Basic にも適用されます。

[スレッド] ビューを使用すると、次のことが行えます。

  • 複数のスレッドの呼び出し履歴の視覚化を表示します。これは、現在のスレッドの呼び出し履歴のみを表示する [呼び出し履歴] ウィンドウよりもアプリの状態の全体像を示します。

  • ブロックされたスレッドやデッドロックされたスレッドなどの問題を特定するのに役立ちます。

C# サンプル

このチュートリアルのサンプル コードは、ゴリラの生活の中で 1 日をシミュレートするアプリケーションを対象としています。 演習の目的は、[並列スタック] ウィンドウの [スレッド] ビューを使用してマルチスレッド アプリケーションをデバッグする方法を理解することです。

このサンプルには、2 つのスレッドが互いに待機しているときに発生するデッドロックの例が含まれています。

呼び出し履歴を直感的にするために、サンプル アプリは次の順番の手順を実行します。

  1. ゴリラを表すオブジェクトを作成します。
  2. ゴリラが目を覚ます。
  3. ゴリラは朝の散歩に行きます。
  4. ゴリラはジャングルでバナナを見つけます。
  5. ゴリラは食べる。
  6. ゴリラはモンキービジネスに従事しています。

マルチスレッドコールスタック

呼び出し履歴の同じセクションがグループ化され、複雑なアプリの視覚化が簡略化されます。

次の概念アニメーションは、グループ化が呼び出し履歴にどのように適用されるかを示しています。 呼び出し履歴の同一セグメントのみがグループ化されます。

呼び出し履歴のグループ化の図。

サンプル プロジェクトを作成する

プロジェクトを作成するには:

  1. Visual Studio を開き、新しいプロジェクトを作成します。

    スタート ウィンドウが開いていない場合は、[ファイル][スタート ウィンドウ]選択します。

    [スタート] ウィンドウで、[ 新しいプロジェクト] を選択します。

    [ 新しいプロジェクトの作成 ] ウィンドウで、検索ボックスに コンソール と入力します。 次に、[言語] の一覧から [C# ] を選択し、[プラットフォーム] の一覧から [Windows ] を選択します。

    言語フィルターとプラットフォーム フィルターを適用した後、.NET 用 コンソール アプリ を選択し、[ 次へ] を選択します。

    Note

    正しいテンプレートが表示されない場合は、 ツール>Get Tools and Features...に移動し、Visual Studio インストーラーを開きます。 [.NET デスクトップ開発] ワークロードを選択し、[変更] を選びます。

    [ 新しいプロジェクトの構成 ] ウィンドウで、名前を入力するか、[ プロジェクト名 ] ボックスに既定の名前を使用します。 その後、 [次へ] を選択します。

    .NET の場合は、推奨されるターゲット フレームワークまたは .NET 8 を選択し、[ 作成] を選択します。

    新しいコンソール プロジェクトが表示されます。 プロジェクトが作成されると、ソース ファイルが表示されます。

  2. プロジェクトで .cs コード ファイルを開きます。 その内容を削除して、空のコード ファイルを作成します。

  3. 選択した言語の次のコードを空のコード ファイルに貼り付けます。

     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");
             }
         }
     }
    

    コード ファイルを更新したら、変更を保存してソリューションをビルドします。

  4. [ファイル] メニューの [すべてを保存] をクリックします。

  5. [ビルド] メニューの [ソリューションのビルド] を選択します。

[並列スタック] ウィンドウの [スレッド] ビューを使用する

デバッグを開始するには:

  1. [ デバッグ ] メニューの [ デバッグの開始 ] (または F5) を選択し、最初の Debugger.Break() がヒットするまで待ちます。

  2. F5 キーを 1 回押すと、デバッガーは同じDebugger.Break()行でもう一度一時停止します。

    これは、2 番目のスレッド内で発生する Gorilla_Start の 2 番目の呼び出しで一時停止します。

    Tip

    デバッガーは、スレッドごとにコードに分割されます。 たとえば、 F5 キーを押して実行を続行し、アプリが次のブレークポイントにヒットすると、別のスレッドのコードに分割される可能性があります。 デバッグ目的でこの動作を管理する必要がある場合は、ブレークポイント、条件付きブレークポイントを追加したり、 すべて中断を使用したりできます。 条件付きブレークポイントの使用の詳細については、「条件付きブレークポイント を使用した 1 つのスレッドのフォロー」を参照してください。

  3. [デバッグ > Windows >並列スタック] を選択して [並列スタック] ウィンドウを開き、ウィンドウの [表示] ドロップダウンから [スレッド] を選択します。

    [並列スタック] ウィンドウの [スレッド] ビューのスクリーンショット。

    スレッド ビューでは、現在のスレッドのスタック フレームと呼び出しパスが青色で強調表示されます。 スレッドの現在の位置が黄色の矢印で表示されます。

    Gorilla_Startの呼び出し履歴のラベルが 2 スレッドであることに注意してください。 最後に F5 キーを押したとき、別のスレッドを開始しました。 複雑なアプリの簡略化のために、同じ呼び出し履歴が 1 つの視覚的表現にグループ化されます。 これにより、特に多くのスレッドを使用するシナリオで、複雑になる可能性のある情報が簡略化されます。

    デバッグ中に、外部コードが表示されるかどうかを切り替えることができます。 機能を切り替えるには、[ 外部コードの表示] を選択またはオフにします。 外部コードを表示する場合でも、このチュートリアルを使用できますが、結果が図とは異なる場合があります。

  4. もう一度 F5 キーを押すと、デバッガーは Debugger.Break() メソッドのMorningWalk行で一時停止します。

    [並列スタック] ウィンドウには、 MorningWalk メソッド内の現在実行中のスレッドの場所が表示されます。

    F5 の後の [スレッド] ビューのスクリーンショット。

  5. グループ化された呼び出し履歴によって表される 2 つのスレッドに関する情報を取得するには、 MorningWalk メソッドにカーソルを合わせます。

    呼び出し履歴に関連付けられているスレッドのスクリーンショット。

    現在のスレッドは、[デバッグ] ツールバーの [スレッド ] の一覧にも表示されます。

    デバッグ ツール バーの現在のスレッドのスクリーンショット。

    [スレッド] リストを使用して、デバッガー コンテキストを別のスレッドに切り替えることができます。 これにより、現在実行中のスレッドは変更されず、デバッガー コンテキストだけが変更されます。

    または、[スレッド] ビューでメソッドをダブルクリックするか、[スレッド] ビューでメソッドを右クリックし、[ フレームに切り替え>[スレッド ID] を選択して、デバッガー コンテキストを切り替えることもできます。

  6. もう一度 F5 キーを押すと、2 番目のスレッドの MorningWalk メソッドでデバッガーが一時停止します。

    2 番目の F5 以降のスレッド ビューのスクリーンショット。

    スレッド実行のタイミングに応じて、この時点で個別の呼び出し履歴またはグループ化された呼び出し履歴が表示されます。

    上の図では、2 つのスレッドの呼び出し履歴が部分的にグループ化されています。 呼び出し履歴の同一のセグメントがグループ化され、矢印線は分離されたセグメントを指します (つまり、同じではありません)。 現在のスタック フレームは、青色の強調表示で示されます。

  7. もう一度 F5 キーを押すと、長い遅延が発生し、[スレッド] ビューに呼び出し履歴情報が表示されません。

    遅延はデッドロックによって発生します。 スレッドがブロックされている場合でも、デバッガーで現在一時停止していないため、スレッド ビューには何も表示されません。

    Tip

    デッドロックが発生した場合、またはすべてのスレッドが現在ブロックされている場合は、[ すべて中断 ] ボタンを使用して呼び出し履歴情報を取得できます。

  8. デバッグ ツール バーの IDE の上部にある [ すべて中断 ] ボタン (一時停止アイコン) を選択するか、 Ctrl + Alt + Break キーを使用します。

    [すべて中断] を選択した後の [スレッド] ビューのスクリーンショット。

    スレッド ビューの呼び出し履歴の上部には、 FindBananas がデッドロックしていることを示しています。 FindBananasの実行ポインターは、現在のデバッガー コンテキストを示す緑色の丸い矢印ですが、スレッドが現在実行されていないことも示します。

    コード エディターで、 lock 関数に緑色のカール矢印が表示されます。 2 つのスレッドは、lock メソッドのFindBananas関数でブロックされます。

    [すべて中断] を選択した後のコード エディターのスクリーンショット。

    スレッドの実行順序に応じて、デッドロックは lock(tree) または lock(banana_bunch) ステートメントに表示されます。

    lockを呼び出すと、FindBananas メソッド内のスレッドがブロックされます。 一方のスレッドは、treeのロックがもう一方のスレッドによって解放されるのを待っていますが、もう一方のスレッドは、banana_bunchのロックを解放する前に、treeのロックが解除されるのを待ちます。 これは、2 つのスレッドが互いに待機しているときに発生するクラシック デッドロックの例です。

    Copilot を使用している場合は、AI によって生成されたスレッドの概要を取得して、潜在的なデッドロックを特定することもできます。

    Copilot スレッドの概要の説明のスクリーンショット。

サンプル コードを修正する

このコードを修正するには、常にすべてのスレッドで一貫性のあるグローバルな順序で複数のロックを取得します。 これにより、循環待機が回避され、デッドロックが排除されます。

  1. デッドロックを修正するには、 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;
    }
    
  2. アプリを再起動します。

Summary

このチュートリアルでは、 並列スタック デバッガー ウィンドウについて説明しました。 マルチスレッド コードを使用する実際のプロジェクトでは、このウィンドウを使用します。 C++、C#、または Visual Basic で記述された並列コードを調べることができます。