メインコンテンツまでスキップ

非同期プログラミング (Async/Await)

非同期プログラミングとは

Webアプリケーションにおいて、データベースアクセスや外部API呼び出しなどのI/O操作(入出力)は、CPU処理に比べて非常に時間がかかります。 同期処理では、これらのI/O操作が完了するまでスレッドがブロック(待機)され、その間CPUは何もできません。

非同期プログラミングを使用すると、I/O操作の待ち時間にスレッドを解放し、他のリクエストを処理できるようになります。これにより、サーバーのスケーラビリティ(同時接続数)が大幅に向上します。

同期処理 vs 非同期処理

  • 同期処理 (Synchronous): 処理が完了するまで呼び出し元が待機する。
  • 非同期処理 (Asynchronous): 処理の完了を待たずに呼び出し元に制御を戻し、完了後に続きを実行する。

基本的な構文: async / await

C#では asyncawait キーワードを使用して、同期コードと同じような構造で非同期コードを記述できます。

// 非同期メソッドの定義には 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;
}