C# の async/await は強力ですが、 正しく使わないと UIフリーズ・デッドロック・例外が消える・処理が走らない など 多くの落とし穴があります。 この記事では、業務アプリで必ず遭遇する問題を体系的にまとめます。
この記事でわかること
・async/await の基本と誤解されやすいポイント
・UIフリーズの原因と対策
・デッドロック(.Result / .Wait)の危険性
・ConfigureAwait の正しい使い方
・Task の誤用(fire-and-forget)
・例外が消える問題と対処
・並列処理(Task.WhenAll)の注意点
・async/await の基本と誤解されやすいポイント
・UIフリーズの原因と対策
・デッドロック(.Result / .Wait)の危険性
・ConfigureAwait の正しい使い方
・Task の誤用(fire-and-forget)
・例外が消える問題と対処
・並列処理(Task.WhenAll)の注意点
1. async/await の基本的な誤解
■ 1-1. async は「非同期になる」ではない
async を付けても、 await するまでは同期処理 です。
public async Task Foo()
{
// ここは同期
await Task.Delay(1000); // ここで初めて非同期
}
■ 1-2. async void は基本禁止
例外が拾えず、呼び出し元に戻れないため、 イベントハンドラ以外で async void は使わない。
2. UIフリーズの原因:.Result / .Wait()
UIアプリ(WPF/WinForms)で最も多い事故
→
.Result や .Wait() を使うとデッドロックして固まる
■ 2-1. 悪い例(UIフリーズ)
var json = http.GetStringAsync(url).Result; // フリーズ
■ 2-2. 正しい例
var json = await http.GetStringAsync(url);
UIスレッドをブロックしないのが async/await の本質です。
3. ConfigureAwait(false) の落とし穴
■ 3-1. ConfigureAwait(false) は UI では使わない
await Task.Delay(1000).ConfigureAwait(false);
これを使うと「UIスレッドに戻らない」ため、 await の後で UI を触ると例外になります。
■ 3-2. 使うべき場所
- ライブラリ
- バックグラウンド処理
- Web API(ASP.NET Core)
UIアプリでは基本使わないのが安全です。
4. fire-and-forget(投げっぱなし)の罠
■ 4-1. 悪い例
DoWorkAsync(); // await しない → 例外が消える
例外が握りつぶされ、処理が終わったかどうかも分からない。
■ 4-2. 正しい例
await DoWorkAsync();
■ 4-3. どうしても fire-and-forget したい場合
_ = Task.Run(async () =>
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Log.Error(ex, "バックグラウンド処理で例外");
}
});
例外を必ずログに残すこと。
5. async void の例外はキャッチできない
■ 5-1. 悪い例
public async void Save()
{
throw new Exception("例外"); // 捕まらない
}
■ 5-2. 正しい例
public async Task SaveAsync()
{
throw new Exception("例外"); // 呼び出し元で捕まる
}
async void はイベントハンドラ専用。
6. Task.WhenAll の落とし穴
■ 6-1. 例外が複数まとめて飛ぶ
await Task.WhenAll(task1, task2, task3);
複数のタスクが失敗すると、 AggregateException になる。
■ 6-2. 個別に例外を扱いたい場合
var tasks = new[] { task1, task2, task3 };
var results = await Task.WhenAll(tasks.Select(t => CatchAsync(t)));
async Task<T?> CatchAsync<T>(Task<T> task)
{
try { return await task; }
catch { return default; }
}
7. async/await とロックの落とし穴
■ 7-1. lock と async は相性が悪い
lock(_lockObj)
{
await Task.Delay(1000); // コンパイルエラー
}
■ 7-2. SemaphoreSlim を使う
await _sem.WaitAsync();
try
{
await Task.Delay(1000);
}
finally
{
_sem.Release();
}
非同期ロックは SemaphoreSlim が正解。
8. async/await のパフォーマンス落とし穴
■ 8-1. 小さな処理に async を付けすぎる
async/await はオーバーヘッドがあるため、 軽い処理に乱用すると逆に遅くなる。
■ 8-2. I/O だけ async にする
- DBアクセス
- ファイルI/O
- API呼び出し
CPU処理は async にしなくてよい。
9. 業務アプリ向けベストプラクティス
- .Result / .Wait() は絶対に使わない(UIフリーズ)
- async void はイベント以外禁止
- ConfigureAwait(false) はUIで使わない
- fire-and-forget は例外ログ必須
- Task.WhenAll の例外は AggregateException に注意
- 非同期ロックは SemaphoreSlim を使う
- I/O だけ async、CPU処理は同期でOK
まとめ:async/await は“正しく使えば最強、誤ると事故る”
- UIフリーズの原因は .Result / .Wait()
- async void は例外が消える
- ConfigureAwait(false) は用途を選ぶ
- Task の誤用で処理が消える・例外が消える
「async/await を使っているのに遅い」「UIが固まる」「例外が消える」 という現場の悩みは、この記事の落とし穴を理解すれば一気に解決します。 あなたのプロジェクトに合わせて、最適な非同期設計を組み立ててみてください。