1. はじめに
コードに飛び込む前に、モチベーションを整理して典型的な状況を想像してみよう。例えば、プログラムがインターネットからファイルをダウンロードしているとする:
// 疑似コード
var data = DownloadFile("https://example.com/file");
ProcessData(data);
問題はこうだ: ダウンロードしている間、プログラムが「固まる」。他の操作は一切できない — ユーザーはマウスを動かせないしボタンを押せないし、ただ終了を待つしかない。見た目は「フリーズした」感じになる。
以前は(そして他の言語でも)この問題を解決するためにスレッド(Thread)、タスク(Task)、デリゲート、タイマーなどを使っていた。コードが複雑になり、読みにくく保守しづらかった。C# チームはこれを簡単にするために async / await を導入した。ほとんど普通のコードを書くように非同期処理が書けるようになったんだ。
従来の「非同期の苦労」 (async/await なし)
対比のために、長い処理を UI をブロックせずに実行するためにスレッドを使った例を見てみよう:
// async/await無しの手動例
var thread = new Thread(() =>
{
var data = DownloadFile("https://example.com/file");
Console.WriteLine("ファイルがダウンロードされました!");
});
thread.Start();
この方法はけっこう原始的で、スレッドを手動で扱う必要があるし、結果を「待つ」簡単な方法もないし、例外処理も面倒になる。
2. C# の非同期: 文法
定義: async と await とは何か?
async — メソッドに付ける修飾子で、そのメソッドを非同期にする。通常は Task(または Task<T>)、もしくは ValueTask を返す。結果は「後で来るよ」という約束(ネット通販の注文みたいなもの)。
await — その行で一時停止して、操作が完了するのを待つけど、スレッドをブロックしないよ、という指示。残りの処理は他に任せられる、というイメージ。
非同期メソッドはどんな感じ?
public async Task MyAsyncMethod()
{
Console.WriteLine("ファイルをダウンロード中...");
var data = await DownloadFileAsync("https://example.com/file"); // 非同期で結果を待つ!
Console.WriteLine("完了!");
}
注意:
- メソッドに async を付けている。
- メソッド内部で非同期操作に対して await を使っている。
- メソッドは Task(戻り値があれば Task<T>)を返す。
可視化: async メソッド呼び出しで何が起きている?
graph LR
A[MyAsyncMethod の呼び出し] --> B[await までの実行]
B --> C[DownloadFileAsync の呼び出し]
C --> D{待機}
D --> |ファイル未ダウンロード| E[スレッドの解放]
D --> |ファイル読み込み済み| F[await 後の実行]
F --> G[メソッドの終了]
- 最初の await まではメソッドは同期的に実行される。
- await のところでメソッドの実行が一時停止し、呼び出し元に制御が戻る。
- 非同期操作が終わると await の後から実行が再開される — まるで何もなかったかのように。
3. 例: async/await を使った非同期ダウンロード
学習用アプリでネットからテキストをダウンロードして、その長さを出力する。ほかのコードはブロックしないようにしてみる。
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
// 非同期メソッド (Task を返す)
public static async Task DownloadAndPrintLengthAsync(string url)
{
Console.WriteLine("ダウンロード開始...");
// HttpClient を使う — 非同期メソッドをサポートしている
using (var client = new HttpClient())
{
string data = await client.GetStringAsync(url);
Console.WriteLine($"ダウンロード完了!テキスト長: {data.Length} 文字。");
}
Console.WriteLine("メソッドの処理が完了しました。");
}
static void Main()
{
// 非同期操作を開始して完了を待つ
var task = DownloadAndPrintLengthAsync("https://www.example.com");
task.Wait(); // 単純なコンソール例では許容される。実際の UI や Web アプリではブロックやデッドロックを引き起こす可能性がある。
}
}
説明:
- DownloadAndPrintLengthAsync — async と await によって完全に非同期になっている。
- メソッド内で非同期ダウンロードの完了を await している。
- Main() ではタスクを起動して Wait() で明示的に待機している。最近の C# では Main 自体を非同期にして (async Task Main) 単純に await できる。
4. 仕組み
同期コードと非同期コードの違い
同期版
Console.WriteLine("開始");
string data = client.GetStringAsync(url).Result; // .Result はスレッドをブロックします!
Console.WriteLine("操作が完了しました");
非同期版
Console.WriteLine("開始");
string data = await client.GetStringAsync(url); // スレッドはブロックされない
Console.WriteLine("操作が完了しました");
await は内部でどう動く?
await を使うと、C# は自動的にメソッドを複数のパートに「分割」する: await までの部分と await の後の部分。呼び出した非同期メソッドが Task を返すと、あなたのメソッドは呼び出し元に戻り、Task が終わると await の後から実行が再開される。これらはすべて自動で行われ、スレッドや切り替えを手動で扱う必要はない。
豆知識: C# は非同期メソッドを内部的に状態マシンに変換し、各 await が新しい「復帰点」になる。
実アプリでの async/await の使い方
static async Task Main(string[] args)
{
var downloadTask = DownloadAndPrintLengthAsync("https://www.example.com");
// ダウンロード中に別の作業をする
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"作業中... イテレーション {i}");
await Task.Delay(500); // 0.5秒の休止、作業を模擬
}
await downloadTask; // ダウンロードの終了を待つ
}
これでプログラムは何かをダウンロードしつつブロックしないで動く: いわば「生きてる」マルチタスク。猫が同時に寝て皿を見張るみたいな感じだね(比喩)。
非同期 ≠ マルチスレッド
初心者がよく混同する重要な点: 非同期コードは同じスレッド上で実行されることがある。非同期処理は「スレッドをブロックしないこと」が目的であって、必ず新しいスレッドを作ることではない。OS のスレッドは高コストなので、非同期はスレッドを解放して他の仕事をさせるための手段として有効(ネットワーク、ファイル、タイマーなどの長時間操作に特に向いている)。
比較表: どのケースで何を選ぶ?
| シナリオ | Thread/Task | async/await |
|---|---|---|
| CPU-bound タスク | はい | はい (Task.Run を経由して) |
| I/O-bound のタスク | 非効率的 | 最適 |
| 大量の並列処理 | 扱いづらい | 簡単 |
| コードの簡潔さ | ややこしい | 読みやすい |
5. 役立つ細かい注意点
非同期の Main
最近の C# では Main を非同期にできる!
static async Task Main(string[] args)
{
// Main の中でそのまま await できる
await DownloadAndAnalyzeFileAsync("https://example.com/file");
}
よくあるミス: await を忘れてタスクが放置される
SomeAsyncFunction(); // await していない、誰も終了を待っていない!
この結果、タスクは「放置」される — もし例外が起きても気づかないことがある。
やってはいけないこと: 同期と非同期を .Result や .Wait() で混ぜる
これはアンチパターンで、非同期の利点を台無しにする。UI や ASP.NET アプリではほぼ確実にデッドロック(相互ブロッキング)を引き起こす: 非同期タスクがスレッドの解放を待っている間に、スレッドがそのタスクの完了を待ってブロックされてしまう。合言葉は覚えておこう: "async all the way"(最後まで非同期で)。
エラー: async void の使用
async void のメソッドは await できないし、そこから投げられた例外は通常の try-catch で捕まえられず、アプリ全体のクラッシュにつながることが多い。許される用途はイベントハンドラだけ(例えば async void Button_Click(...))で、その他は async Task を使うべき。
エラー: await がないのに余分な async
メソッドに async を付けても中で一度も await を使わなければ、コンパイラは警告を出す。そういうコードは同期的に実行されるのに不要な状態マシンが作られて無駄が出る。混乱の元だし、パフォーマンスも落ちる。
短いルールとよくある質問
- await を async でない場所で使えますか? 使えません。常に async が付いたメソッド内だけ。
- 1つのメソッドに複数の await は使えますか? はい。何個でも使えます — それぞれが待機ポイントになります。
- 結果を返したい場合は? Task<T> を使い、通常の return を書きます。
- 複数の非同期タスクを組み合わせられますか? もちろん。複数のタスクを並列で起動して Task.WhenAll でまとめて待つ、などができる。
6. async/await 使用時の典型的なミス
エラー №1: "火の玉" — 非同期タスクの呼び出し後に await しない
DownloadAndPrintLengthAsync("https://www.example.com");
Console.WriteLine("すべて完了!"); // 実際にはダウンロードはまだ続いている!
コードは非同期処理の完了を待たない。非同期タスクは必ず await するか、明示的に Wait() する必要がある(ただし後者は危険でブロックの原因になる)。
エラー №2: 同期と非同期を .Result や .Wait() で混ぜる
これはアンチパターンで、非同期の利点を打ち消す。UI や ASP.NET ではデッドロックの原因となる。覚えておこう: "async all the way"。
エラー №3: async void の使用
async void は await できないし、そこから出た例外は通常の try-catch で捕れず、アプリが強制終了することが多い。イベントハンドラ以外では避け、代わりに async Task を使う。
エラー №4: await がないのに余分な async
メソッドに async を付けているのに内部で await を使っていないと、コンパイラが警告を出す。完全に同期で実行されるのに不必要な状態マシンが作られてしまうのでやめよう。
GO TO FULL VERSION