【Visual Studio C# デバッグ入門】非同期コードのデバッグ方法を徹底解説 「例外処理・ブレークポイント編」

C#の非同期プログラミング(async/await)は、UIの応答性を保ったり、バックグラウンド処理を効率化する際に非常に役立ちます。

しかし、非同期コードのデバッグは慣れるまではやや難解に感じるかもしれません。

今回はコンソールアプリを例にして、非同期処理(async/await)のデバッグ方法「例外処理・ブレークポイント」について具体的に紹介します。

  • 対象者
    • コンソールアプリで async/await を使い始めた初〜中級者
    • 非同期処理の例外やブレークポイント操作を学びたい方

同期メソッドと非同期メソッドのデバッグの違い

同期メソッドと非同期メソッドの主な違いを下表にまとまめしたが、非同期メソッドの最大の注意点は「await の前後で処理の流れが途切れるように見える」ことです。

観点同期メソッド非同期メソッド
(async/await)
1実行の流れ順次的に進行タスクを分割してスレッドを解放
2ブレークの挙動単純な流れを追えるawait 前後でスレッドが切り替わる可能性あり
3コールスタックの見え方そのまま見えるasync の内部状態が特殊な構造になる
4ステップ実行の動き直線的に進むawait 後はジャンプしたように見える場合あり

非同期プログラム(コンソールアプリ)のデバッグ方法

下記のサンプルプログラムは非同期に HTTP リクエストを送信し、外部の Web API(https://jsonplaceholder.typicode.com/posts/1)からデータを取得。そのレスポンスをコンソールに表示するサンプルです。

このURLは「投稿(posts)」の中の IDが1の投稿データ を取得します。アクセスすると、以下のようなJSON形式のデータが返ってきます。

{
“userId”: 1,
“id”: 1,
“title”: “sunt aut facere repellat provident occaecati excepturi optio reprehenderit”,
“body”: “quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto”
}

※ここではスレッドの切り替わりを確認する為に、await の前後にスレッドIDを表示する命令を挿入しています。

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("処理開始");
        try
        {
Console.WriteLine($"Main開始スレッドID: {Thread.CurrentThread.ManagedThreadId}");
            var result = await FetchDataAsync("https://jsonplaceholder.typicode.com/posts/1");
Console.WriteLine($"Main終了スレッドID: {Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine("取得結果:");
            Console.WriteLine(result);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"エラー発生: {ex.Message}");
        }
        Console.WriteLine("処理終了");
    }

    static async Task<string> FetchDataAsync(string url)
    {
        using var httpClient = new HttpClient();
Console.WriteLine($"GetAsync実行前ID: {Thread.CurrentThread.ManagedThreadId}");
        var response = await httpClient.GetAsync(url);
Console.WriteLine($"GetAsync実行ID: {Thread.CurrentThread.ManagedThreadId}");
        response.EnsureSuccessStatusCode(); // ステータスコードがエラーなら例外を発生
        return await response.Content.ReadAsStringAsync();
    }
}

ブレークポイントの使い方

  • await前後両方にブレークポイントを置くのが基本。
    • 処理(スレッド)の切り替わりを追いやすいくなります。

上記サンプルプログラムを実行すると以下のようになります。

「httpClient.GetAsync(url);」 命令実行後にスレッドが切り替わっているのが分かります。

非同期処理での例外の捕捉

非同期メソッド内で発生した例外は、通常の try-catch で捕捉できますが、例外が発生する場所までステップインしないと、内部の詳細が見えにくいことがあります。

今回は「ネットワークエラー」を発生させて、例外が発生した瞬間にブレークできるよう以下の設定を行います。

  • 設定方法
    • メニューの「デバッグ」→「ウィンドウ」→「例外設定」で、System.Net.Http.HttpRequestException をチェック

他の例外でも、対象となる例外設定にチェックを入れると例外が発生した瞬間にブレークできるようになります。

サンプルプログラムで使用しているURLを存在しないURLに変更して実行してみると、「response.EnsureSuccessStatusCode(); // ステータスコードがエラーなら例外を発生」の箇所でブレークします。

  • デバッグのポイント
    • ブレークポイントの設定箇所
      • 「await httpClient.GetAsync(url);」の前後
      • catch (Exception ex) の中
    • ウォッチする変数
      • url: 正しく設定されているか?
      • response:null になる可能性があるか?
      • ex.Message:実際のエラーメッセージを確認

非同期例外のデバッグTips

  • wait での例外はメソッド外に伝播するので、try-catch を呼び出し元にも必ず書く
  • テータスコードが 404 や 500 のような「正常なHTTPレスポンス」は GetAsync() で例外を投げませんが、EnsureSuccessStatusCode() で例外になる
  • 存在しないホスト名などは DNS で即座に例外となるため、確実に例外デバッグが行えます

ログ出力による追跡

  • デバッグログを挟むことで、await 前後の流れや問題の特定がしやすくなります。
  • 本格的には SerilogNLog を使ってログをファイル出力するのもおすすめです。

■Serilog や NLog の使用方法は下記ブログを参照下さい

ステップ実行と動作の巻き戻し

  • ステップ実行
    • F10:ステップオーバー
    • F11:ステップイン(async関数内に入れる)
    • Shift + F11:ステップアウト

注意点await をステップインすると、生成されたステートマシン(裏側のコード)に入ることがあり、慣れないうちは混乱します。

  • 動作の巻き戻し
    • ブレーク時のブレークポイントをクリックしたまま、実行を戻したい場所までドラッグして放す
    • 黄色い⇒が表示され、そこからステップ実行が可能となる。但し、状態を完全に巻き戻すわけではないので注意が必要。

下記は、①でブレークした状態を②まで実行を巻き戻した例。②からステップ実行実行が可能となる。

呼び出し履歴(コールスタック)の活用

非同期コードではスタックの流れが分かりづらくなるため、コールスタックを活用する事も重要です。

  • どのawaitから来たか(呼び出し元)を追える
    • Main → FetchDataAsync → ParseJsonAsync のように非同期でも呼び出し経路を視覚化できる
  • 非同期メソッド間の流れが分断されない
    • 途中のawaitで中断されても、再開された処理の文脈が残っている
    • 「このawaitの後に、なぜここに来たのか?」を確認できる
  • 例外が発生した場所と、呼び出し元を一度に確認できる
    • 非同期処理中に例外が発生したとき、どの関数から呼ばれ、最終的にどこで例外がスローされたかがすぐに分かる

呼び出し履歴(コールスタック)の使い方

  1. 非同期メソッド内でブレークポイント停止
  2. メニュー → [デバッグ] → [ウィンドウ] → コールスタック(または Ctrl + Alt + C
  3. 一番下が起点、一番上が現在の位置
  4. 各メソッドをダブルクリックすると、ソースにジャンプ可能

まとめ

非同期処理でも以下の点を押さえておけば効率的なデバッグが可能です。

  • await の前後にブレークポイント
  • ウォッチやクイックウォッチで変数確認
  • ステップ実行(F10/F11)で処理を追う
  • 例外ウィンドウでエラーの瞬間を補足
  • 呼び出し履歴(コールスタック)を活用

タイトルとURLをコピーしました