りおんクロニクル


C# async/await の落とし穴|UIフリーズ・デッドロック・例外・Taskの罠を徹底解説【2026年版】

Home【2026年版】C# / .NET入門と実践ガイド|基礎・業務アプリ開発・SQLite連携まで体系的に解説

C# の async/await は強力ですが、 正しく使わないと UIフリーズ・デッドロック・例外が消える・処理が走らない など 多くの落とし穴があります。 この記事では、業務アプリで必ず遭遇する問題を体系的にまとめます。

この記事でわかること
・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. 使うべき場所

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 にする

CPU処理は async にしなくてよい。

9. 業務アプリ向けベストプラクティス

まとめ:async/await は“正しく使えば最強、誤ると事故る”

「async/await を使っているのに遅い」「UIが固まる」「例外が消える」 という現場の悩みは、この記事の落とし穴を理解すれば一気に解決します。 あなたのプロジェクトに合わせて、最適な非同期設計を組み立ててみてください。

前のページ  次のページ