• システム開発に関わる内容をざっくりと書いていく

C# async await デッドロック・待ち続けるときの見方

C# の async/await で「終わらない」「UI やリクエストが固まる」ときは、例外より待ち方の設計が原因なことが多い。

この記事では、デッドロックっぽく見える待ちと、よくある直し方をざっくり整理する。


先に結論

  • ASP.NET Core では原則 await を貫通させる
  • Task.Result / Wait() で同期待ちしない
  • 古い同期コンテキスト前提のコードと混ぜると詰まりやすい

よくあるパターン

1. async なのに Result / Wait で待つ

// NG例(同期待ち)
var data = GetDataAsync().Result;
GetDataAsync().Wait();

待っているスレッドと、続きを実行したいスレッドがぶつかり、進まなくなることがある。

2. 一部だけ async で、境界で同期に戻す

public string GetName()
{
return GetNameAsync().GetAwaiter().GetResult(); // 境界で同期化
}

ライブラリ境界などどうしても必要な場合以外は避けた方が安全。

3. 並行実行の待ち合わせ不足 / 過多

var t1 = LoadAAsync();
var t2 = LoadBAsync();
await Task.WhenAll(t1, t2);

逆に、順番に await しすぎて遅くなるケースもある。固まる系と遅い系は切り分けて考える。


切り分けの見方

  1. スタックを見て Wait / Result / GetResult がないか確認
  2. デッドロックなのか、単に外部 I/O 待ちで遅いのかを分ける
  3. 再現が特定リクエストだけなら、同期 API との境目を疑う
  4. 可能なら全部 async でつなぎ、同期待ちを消す

対策

  • コントローラ〜インフラまで async Task で通す
  • 同期 API しかない場合は、設計を見直す(無理にラップして隠蔽しない)
  • タイムアウト付き CancellationToken で無限待ちを防ぐ
  • ログに「どこで待っているか」を残す
public async Task<IActionResult> GetAsync(CancellationToken ct)
{
var data = await _service.GetAsync(ct);
return Ok(data);
}

ざっくりまとめ

  • 終わらない async は、例外より待ち方を疑う
  • Result / Wait は第一容疑者
  • ASP.NET Core では await を貫通させるのが基本