次の方法で共有


非同期アプリケーションをデバッグする

このチュートリアルでは、 並列スタック ウィンドウの [タスク] ビューを使用して C# 非同期アプリケーションをデバッグする方法について説明します。 このウィンドウは、非同期/待機パターン ( タスク ベースの非同期パターン (TAP) とも呼ばれます) を使用するコードの実行時の動作を理解して確認するのに役立ちます。

タスク並列ライブラリ (TPL) を使用しているが、非同期/待機パターンを使用しないアプリの場合、またはコンカレンシー ランタイムを使用する C++ アプリの場合は、[並列スタック] ウィンドウの [スレッド] ビューがデバッグに最も役立つツールです。 詳細については、「[並列スタック] ウィンドウでスレッドを表示する」を参照してください。

[タスク] ビューは、次の作業に役立ちます。

  • async/await パターンを使用するアプリの呼び出し履歴の視覚化を表示します。 これらのシナリオでは、[タスク] ビューでアプリの状態をより完全に把握できます。

  • 実行するようにスケジュールされているが、まだ実行されていない非同期コードを特定します。 たとえば、データを返していない HTTP 要求は、[スレッド] ビューではなく [タスク] ビューに表示される可能性が高く、問題を特定するのに役立ちます。

  • 同期オーバー非同期パターンなどの問題と、ブロックされたタスクや待機中のタスクなどの潜在的な問題に関連するヒントを特定するのに役立ちます。 同期オーバー非同期コード パターンは、非同期メソッドを同期方式で呼び出すコードを指します。これは、スレッドをブロックすることがわかっており、スレッド プールの枯渇の最も一般的な原因です。

C# のサンプル

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

このサンプルには、同期オーバー非同期アンチパターンを使用する例が含まれています。この例では、スレッド プールの不足が発生する可能性があります。

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

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

非同期呼び出しスタック

並列スタックの [タスク] ビューには非同期呼び出し履歴の視覚化が用意されているため、アプリケーションで何が起こっているか (または発生する予定) を確認できます。

タスク ビューでデータを解釈する際に覚えておく必要がある重要な点をいくつか次に示します。

  • 非同期呼び出し履歴は論理呼び出し履歴または仮想呼び出し履歴であり、スタックを表す物理呼び出し履歴ではありません。 非同期コードを使用する場合 (たとえば、 await キーワードを使用)、デバッガーは "非同期呼び出し履歴" または "仮想呼び出し履歴" のビューを提供します。 非同期呼び出し履歴は、非同期呼び出し履歴が物理スレッドで現在実行されているとは限らないため、スレッドベースの呼び出し履歴 ("物理スタック") とは異なります。 そうではなく、非同期コール スタックは、将来的に非同期で実行されるコードの継続または "約束" です。 コール スタックは、継続を使用して作成されます。

  • スケジュールされているが現在実行中ではない非同期コードは、物理呼び出し履歴には表示されませんが、非同期呼び出し履歴のタスク ビューに表示されます。 .Wait.Resultなどのメソッドを使用してスレッドをブロックしている場合は、代わりに物理呼び出し履歴にコードが表示されることがあります。

  • 非同期仮想呼び出しスタックは、 .WaitAny.WaitAllなどのメソッド呼び出しの使用によって生じる分岐のため、常に直感的であるとは限りません。

  • [呼び出し履歴] ウィンドウは、現在実行中のスレッドの物理呼び出し履歴を表示するため、[タスク] ビューと組み合わせて使用すると便利です。

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

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

    仮想コールスタックのグループ化の図解。

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

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

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

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

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

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

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

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

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

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

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

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

    using System.Diagnostics;
    
    namespace AsyncTasks_SyncOverAsync
    {
         class Jungle
         {
             public static async Task<int> FindBananas()
             {
                 await Task.Delay(1000);
                 Console.WriteLine("Got bananas.");
                 return 0;
             }
    
             static async Task Gorilla_Start()
             {
                 Debugger.Break();
                 Gorilla koko = new Gorilla();
                 int result = await Task.Run(koko.WakeUp);
             }
    
             static async Task Main(string[] args)
             {
                 List<Task> tasks = new List<Task>();
                 for (int i = 0; i < 2; i++)
                 {
                     Task task = Gorilla_Start();
                     tasks.Add(task);
    
                 }
                 await Task.WhenAll(tasks);
    
             }
         }
    
         class Gorilla
         {
    
             public async Task<int> WakeUp()
             {
                 int myResult = await MorningWalk();
    
                 return myResult;
             }
    
             public async Task<int> MorningWalk()
             {
                 int myResult = await Jungle.FindBananas();
                 GobbleUpBananas(myResult);
    
                 return myResult;
             }
    
             /// <summary>
             /// Calls a .Wait.
             /// </summary>
             public void GobbleUpBananas(int food)
             {
                 Console.WriteLine("Trying to gobble up food synchronously...");
    
                 Task mb = DoSomeMonkeyBusiness();
                 mb.Wait();
    
             }
    
             public async Task DoSomeMonkeyBusiness()
             {
                 Debugger.Break();
                 while (!System.Diagnostics.Debugger.IsAttached)
                 {
                     Thread.Sleep(100);
                 }
    
                 await Task.Delay(30000);
                 Console.WriteLine("Monkey business done");
             }
         }
    }
    

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

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

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

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

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

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

    これは、2 番目の非同期タスク内で発生する、 Gorilla_Startの 2 番目の呼び出しで一時停止します。

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

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

    非同期呼び出し履歴のラベルに 2 つの非同期論理スタックが記述されていることに注意してください。 最後に F5 キーを押したとき、別のタスクを開始しました。 複雑なアプリの簡略化のために、同じ非同期呼び出し履歴が 1 つの視覚的表現にグループ化されます。 これにより、特に多くのタスクを含むシナリオで、より完全な情報が提供されます。

    [タスク] ビューとは対照的に、[ 呼び出し履歴] ウィンドウには、複数のタスクではなく、現在のスレッドのみの呼び出し履歴が表示されます。 多くの場合、両方を一緒に表示して、アプリの状態をより詳しく把握すると便利です。

    呼び出し履歴のスクリーンショット。

    ヒント

    [呼び出し履歴] ウィンドウには、説明 Async cycleを使用して、デッドロックなどの情報を表示できます。

    デバッグ中に、外部コードが表示されるかどうかを切り替えることができます。 機能を切り替えるには、[呼び出し履歴] ウィンドウの [名前] テーブル ヘッダーを右クリックし、[外部コードの表示] を選択またはオフにします。 外部コードを表示する場合でも、このチュートリアルを使用できますが、結果が図とは異なる場合があります。

  4. もう一度 F5 キーを押すと、デバッガーが DoSomeMonkeyBusiness メソッドで一時停止します。

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

    このビューには、内部継続チェーンに非同期メソッドが追加された後の、より完全な非同期呼び出し履歴が表示されます。これは、 await および同様のメソッドを使用するときに発生します。 DoSomeMonkeyBusiness 非同期メソッドですが、継続チェーンにまだ追加されていないため、非同期呼び出しスタックの先頭に存在する場合と存在しない場合があります。 次の手順でこれが当てはまる理由について説明します。

    このビューには、 Jungle.MainStatus Blocked のブロックアイコンも表示されます。 これは有益ですが、通常は問題を示していません。 ブロックされたタスクとは、別のタスクの完了、通知されるイベント、または解放されるロックを待機しているためにブロックされるタスクです。

  5. GobbleUpBananas メソッドにカーソルを合わせると、タスクを実行している 2 つのスレッドに関する情報が取得されます。

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

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

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

    [スレッド] リストを使用して、デバッガー コンテキストを別のスレッドに切り替えることができます。

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

    2 番目の F5 以降の [タスク] ビューのスクリーンショット。

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

    上の図では、2 つのタスクの非同期呼び出し履歴は同一ではないので、別々です。

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

    遅延は、実行時間の長いタスクによって発生します。 この例では、Web 要求などの実行時間の長いタスクをシミュレートするため、スレッド プールの枯渇が発生する可能性があります。 タスクがブロックされている場合でも、デバッガーで現在一時停止していないため、タスク ビューには何も表示されません。

    ヒント

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

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

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

    タスク ビューの非同期呼び出し履歴の上部付近に、 GobbleUpBananas がブロックされていることがわかります。 実際には、2 つのタスクが同じ時点でブロックされます。 ブロックされたタスクは必ずしも予期せず、必ずしも問題があることを意味するとは限りません。 ただし、観察された実行遅延は問題を示し、ここでの呼び出し履歴情報は問題の場所を示しています。

    前のスクリーンショットの左側の緑色の矢印は、現在のデバッガー コンテキストを示しています。 2 つのタスクは、mb.Wait() メソッドのGobbleUpBananasでブロックされます。

    [呼び出し履歴] ウィンドウには、現在のスレッドがブロックされていることも示されます。

    [すべて中断] を選択した後の呼び出し履歴のスクリーンショット。

    Wait()を呼び出すと、GobbleUpBananasへの同期呼び出し内のスレッドがブロックされます。 これは同期オーバー非同期アンチパターンの例であり、これが UI スレッドまたは大規模な処理ワークロードで発生した場合は、通常、 awaitを使用したコード修正で対処されます。 詳細については、「 スレッド プールの不足をデバッグする」を参照してください。 プロファイリング ツールを使用してスレッド プールの不足をデバッグするには、「 ケース スタディ: パフォーマンスの問題を分離する」を参照してください。

    興味深いことに、DoSomeMonkeyBusiness は呼び出し履歴に表示されません。 現在スケジュールされており、実行されていないため、[タスク] ビューの非同期呼び出し履歴にのみ表示されます。

    ヒント

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

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

  1. GobbleUpBananas メソッドを次のコードに置き換えます。

     public async Task GobbleUpBananas(int food) // Previously returned void.
     {
         Console.WriteLine("Trying to gobble up food...");
    
         //Task mb = DoSomeMonkeyBusiness();
         //mb.Wait();
         await DoSomeMonkeyBusiness();
     }
    
  2. MorningWalk メソッドで、awaitを使用して GobbleUpBananas を呼び出します。

    await GobbleUpBananas(myResult);
    
  3. [再起動] ボタン (Ctrl + Shift + F5) を選択し、アプリが "ハング" と表示されるまで F5 キーを数回押します。

  4. [すべて中断] を押します

    今回は、 GobbleUpBananas は非同期的に実行されます。 中断すると、非同期コール スタックが表示されます。

    コード修正後のデバッガー コンテキストのスクリーンショット。

    [呼び出し履歴] ウィンドウは、 ExternalCode エントリを除いて空です。

    コード エディターには何も表示されません。ただし、すべてのスレッドが外部コードを実行していることを示すメッセージが表示される点が異なります。

    ただし、[タスク] ビューには役立つ情報が表示されます。 DoSomeMonkeyBusiness は、想定どおりに非同期呼び出し履歴の一番上にあります。 これにより、実行時間の長いメソッドが配置されている場所が正しく通知されます。 これは、[呼び出し履歴] ウィンドウの物理呼び出し履歴で十分な詳細が提供されていない場合に、async/await の問題を分離するのに役立ちます。

概要

このチュートリアルでは、 並列スタック デバッガー ウィンドウについて説明しました。 async/await パターンを使用するアプリでは、このウィンドウを使用します。