C#の非同期処理 — async/awaitとTaskの仕組み
この章の目次開く
Webアプリやツールの処理時間の多くは、計算ではなく待ち時間です。データベースの応答、外部APIのレスポンス、ファイルの読み込み。この待ち時間の間もスレッドを占有し続けると、同時に処理できる件数が頭打ちになります。
C#の async / await は、この待ち時間にスレッドを解放するための仕組みです。JavaScriptの async / await と書き方はほぼ同じですが、背景にある仕組みは異なります。
基本の書き方
public async Task<string> FetchAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}| 要素 | 意味 |
|---|---|
async | このメソッドの中で await を使う、という宣言 |
Task<T> | 「いずれ T を返す処理」を表す型 |
Task | 「いずれ完了する処理」(戻り値なし) |
await | 完了を待つ。待っている間はスレッドを手放す |
Task はJavaScriptの Promise に相当します。「実行中の処理そのもの」を値として扱えるようにしたものです。
| C# | JavaScript |
|---|---|
Task<string> | Promise<string> |
await | await |
Task.WhenAll | Promise.all |
Task.WhenAny | Promise.race |
Task.Delay(1000) | setTimeout のPromise版 |
awaitしている間に何が起きているか
await に到達すると、メソッドはそこで一旦戻り、スレッドは他の仕事に回されます。待っていた処理が終わると、続きから再開されます。同時に多数のリクエストを捌けるのは、この仕組みのおかげです。
学習者「速くなる」ものだと思っていましたが、1つの処理自体が速くなるわけではないんですね。
その通りです。1件あたりの所要時間は変わりません。変わるのは同時に扱える件数です。待ち時間にスレッドを占有しないため、少ないスレッドで多くのリクエストを処理できます。

複数の処理を同時に走らせる
順番に await すると、処理は直列になります。
var a = await FetchAsync(url1); // 1秒
var b = await FetchAsync(url2); // さらに1秒 → 合計2秒同時に走らせたい場合は、先にTaskを作ってから待ちます。
構文: Task.WhenAll(tasks)
| 引数 | 渡せるもの | 説明 |
|---|---|---|
tasks(第1引数) | Task の配列 / IEnumerable<Task> | 完了を待ちたい処理の集まり |
戻り値: すべての完了を表す Task。Task<T> を渡した場合は結果の配列(Task<T[]>)
var task1 = FetchAsync(url1);
var task2 = FetchAsync(url2);
var results = await Task.WhenAll(task1, task2); // 合計1秒程度非同期処理での例外
await した時点で、中で発生した例外が投げ直されます。try / catch は同期処理と同じように書けます。
try
{
var text = await FetchAsync(url);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"取得に失敗: {ex.Message}");
}Task.WhenAll で複数のタスクが失敗した場合、await が投げるのは最初の1件です。すべての例外を見たい場合は、タスクの Exception プロパティを確認します。
キャンセルに対応する
時間のかかる処理には、CancellationToken を引数で受け取り、そのまま渡していくのが標準的な作法です。
public async Task<string> FetchAsync(string url, CancellationToken cancellationToken)
{
using var client = new HttpClient();
var response = await client.GetAsync(url, cancellationToken);
return await response.Content.ReadAsStringAsync(cancellationToken);
}ASP.NET Coreでは、クライアントが接続を切ったときにこのトークンがキャンセル状態になります。受け取って渡すだけで、無駄な処理を続けずに済みます。
よくあるハマりどころ
.Result や .Wait() で待つ
var text = FetchAsync(url).Result; // 危険非同期の完了を同期的に待つと、環境によってはデッドロックが発生し、アプリが応答しなくなります。呼び出し元も async にして await するのが原則です。
async で通します。
async void を使う
public async void Run() // 避けるasync void のメソッドは呼び出し側が完了を待てず、内部で発生した例外も捕捉できません(多くの場合プロセスが落ちます)。イベントハンドラー以外では async Task を使います。
await を付け忘れる
SaveAsync(user); // 警告は出るが、完了を待たずに次へ進む処理が終わる前にメソッドを抜けてしまい、「保存されていないのにエラーも出ない」という状況になります。警告を無視しないことが対策です。
CPUを使う処理を非同期にすれば速くなると考える
async / await が効くのは、待ち時間があるI/O処理です。計算そのものを速くしたい場合は、Task.Run で別スレッドに逃がす、あるいは並列処理(Parallel.ForEach)を検討します。
先生「待つ処理」は async、「計算する処理」は並列化。この区別ができていれば大きく外さないよ。
同期版と非同期版を混ぜる
File.ReadAllText と File.ReadAllTextAsync のように、多くのAPIには両方の版があります。非同期のメソッドの中では、非同期版を使い切ってください。
ちゃんと使うためのポイント
TaskはJavaScriptのPromiseにあたる「実行中の処理」を表す型awaitしている間はスレッドが解放され、同時処理数が増える- 同時に走らせたいときは先にTaskを作り、
Task.WhenAllで待つ .Result/.Wait()/async voidは避ける- 時間のかかる処理は
CancellationTokenを受け取り、そのまま渡す - 非同期が効くのはI/O待ち。計算の高速化は別の手段を使う
次の章では、Task<T> や List<T> の <T> の正体であるジェネリクスを扱います。ここまで何度も出てきた記法を、自分で書けるようにします。