非同期プログラミング (Async/Await)
非同期プログラミングとは
Webアプリケーションにおいて、データベースアクセスや外部API呼び出しなどのI/O操作(入出力)は、CPU処理に比べて非常に時間がかかります。 同期処理では、これらのI/O操作が完了するまでスレッドがブロック(待機)され、その間CPUは何もできません。
非同期プログラミングを使用すると、I/O操作の待ち時間にスレッドを解放し、他のリクエストを処理できるようになります。これにより、サーバーのスケーラビリティ(同時接続数)が大幅に向上します。
同期処理 vs 非同期処理
- 同期処理 (Synchronous): 処理が完了するまで呼び出し元が待機する。
- 非同期処理 (Asynchronous): 処理の完了を待たずに呼び出し元に制御を戻し、完了後に続きを実行する。
基本的な構文: async / await
C#では async と await キーワードを使用して、同期コードと同じような構造で非同期コードを記述できます。
// 非同期メソッドの定義には async キーワードを付ける
// 戻り値は通常 Task または Task<T>
public async Task<User> GetUserAsync(int userId)
{
// I/O操作(DBアクセスなど)の前に await を付ける
// これにより、完了までスレッドが解放される
var user = await _userRepository.FindByIdAsync(userId);
return user;
}
戻り値の型
| 型 | 用途 |
|---|---|
Task | 戻り値のない非同期メソッド(同期メソッドの void に相当) |
Task<T> | 値 T を返す非同期メソッド |
ValueTask<T> | パフォーマンス最適化が必要な場合(メモリ割り当てを減らす) |
void | イベントハンドラ以外では使用しない(例外をキャッチできなくなるため) |
ベストプラクティス
1. async void を避ける
async void メソッド内で例外が発生すると、呼び出し元でキャッチできず、プロセスがクラッシュする可能性があります。
イベントハンドラ(ボタンクリックなど)以外では、必ず Task を返すようにします。
❌ Bad Code
public async void SaveData() // 危険!
{
await _db.SaveChangesAsync();
}
✅ Good Code
public async Task SaveDataAsync()
{
await _db.SaveChangesAsync();
}
2. デッドロックの回避 (.Result / .Wait を使わない)
非同期メソッドを同期的に呼び出すために .Result や .Wait() を使用すると、デッドロックが発生する可能性があります(特にASP.NETやWPFなどの同期コンテキストを持つ環境)。
常に await を使用して呼び出します。
❌ Bad Code
var user = GetUserAsync(1).Result; // デッドロックの危険性あり
✅ Good Code
var user = await GetUserAsync(1);
3. ConfigureAwait(false)
ライブラリ開発などでは、ConfigureAwait(false) を使用して、元のコンテキスト(UIスレッドなど)に戻る必要がないことを明示することで、パフォーマンスを向上させ、デッドロックを回避できます。
※ ASP.NET Core アプリケーションコードでは、同期コンテキストがないため必須ではありませんが、ライブラリコードでは重要です。
await _fileStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
4. CPUバウンドな処理は Task.Run で
重い計算処理(CPUバウンド)を非同期メソッド内でそのまま実行すると、呼び出し元のスレッド(UIスレッドなど)をブロックしてしまいます。
Task.Run を使用して、明示的にスレッドプール上の別スレッドで実行するようにします。
// CPUバウンドな重い処理
public async Task<double> CalculateHeavyStatisticsAsync(List<Data> data)
{
// 別スレッドで実行
return await Task.Run(() =>
{
// 重い計算ロジック
return PerformComplexCalculation(data);
});
}
5. コンストラクタでの非同期呼び出しを避ける
コンストラクタは同期メソッドであり、await を使用できません。
コンストラクタ内で Task.Run や .Result を使うのはアンチパターンです。
代わりに「非同期ファクトリーメソッド」パターンを使用します。
public class Connection
{
private Connection() { } // privateコンストラクタ
// 非同期ファクトリーメソッド
public static async Task<Connection> CreateAsync()
{
var instance = new Connection();
await instance.InitializeAsync(); // 非同期初期化
return instance;
}
private async Task InitializeAsync()
{
// 接続処理など
}
}
並列処理 (Task.WhenAll)
複数の独立した非同期操作がある場合、それらを順番に await するのではなく、同時に開始して全て完了するのを待つことで、全体の処理時間を短縮できます。
public async Task<DashboardData> GetDashboardDataAsync()
{
// タスクを開始する(まだ await しない)
var userTask = _userService.GetUserAsync();
var ordersTask = _orderService.GetOrdersAsync();
var newsTask = _newsService.GetNewsAsync();
// 全てのタスクが完了するのを待つ
await Task.WhenAll(userTask, ordersTask, newsTask);
// 結果を取得
return new DashboardData
{
User = await userTask, // 既に完了しているので即座に値が返る
Orders = await ordersTask,
News = await newsTask
};
}
キャンセル処理 (CancellationToken)
長時間実行される処理や、ユーザーが中断したリクエストを適切にキャンセルするために、CancellationToken を使用します。
// コントローラーのアクションメソッド
public async Task<IActionResult> GetData(CancellationToken cancellationToken)
{
// サービスメソッドにトークンを渡す
var data = await _service.GetDataAsync(cancellationToken);
return Ok(data);
}
// サービスメソッド
public async Task<Data> GetDataAsync(CancellationToken cancellationToken)
{
// EF Coreなどのメソッドにトークンを渡す
var result = await _dbContext.Data
.ToListAsync(cancellationToken); // キャンセルされると例外がスローされる
return result;
}