非同期処理において、複数の独立した処理(例:API AとAPI Bの両方からデータを取得する)を行う際、それらを順番に await していくと、処理時間が「足し算」になってしまい非効率です。
Task.WhenAll メソッドを使用すると、複数のタスクを同時に(並列に)開始し、「すべてのタスクが完了するまで待機する」 という制御を効率的に実装できます。これにより、全体の処理時間は「最も遅いタスクの時間」に短縮されます。
Task.WhenAllによる並列実行の実装
以下のサンプルコードでは、3つの異なる初期化処理(データベース接続、キャッシュ読み込み、設定ファイル取得)を並列に実行するシナリオをシミュレーションします。
System.Diagnostics.Stopwatch を使用して、順次実行した場合と比べてどれだけ時間が短縮されているかを確認できるようにしています。
サンプルコード
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
public class Program
{
public static async Task Main()
{
Console.WriteLine("アプリケーションの初期化を開始します...");
var stopwatch = Stopwatch.StartNew();
// 1. 複数のタスクを定義(この時点で実行が開始されます)
// ※ここではまだ await しません
Task<string> dbTask = InitializeDatabaseAsync();
Task<string> cacheTask = LoadCacheAsync();
Task<string> configTask = FetchConfigAsync();
// タスクのリストを作成
var allTasks = new List<Task<string>> { dbTask, cacheTask, configTask };
try
{
Console.WriteLine("--- 全タスク並列実行中 ---");
// 2. Task.WhenAll で全てのタスクが完了するのを待機
// 戻り値は、各タスクの結果(string)が格納された配列になります
string[] results = await Task.WhenAll(allTasks);
stopwatch.Stop();
Console.WriteLine("\n--- 全処理完了 ---");
Console.WriteLine($"経過時間: {stopwatch.Elapsed.TotalSeconds:F2}秒");
// 理論上、最も長いタスク(3秒)に近い時間で終わるはずです
// (順次実行なら 3+2+1 = 6秒 かかる処理)
// 結果の表示
foreach (var result in results)
{
Console.WriteLine($"結果: {result}");
}
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
// 擬似的な重い処理 1 (3秒)
static async Task<string> InitializeDatabaseAsync()
{
await Task.Delay(3000); // 3秒待機
Console.WriteLine("[完了] データベース接続");
return "DB:OK";
}
// 擬似的な重い処理 2 (2秒)
static async Task<string> LoadCacheAsync()
{
await Task.Delay(2000); // 2秒待機
Console.WriteLine("[完了] キャッシュ読み込み");
return "Cache:OK";
}
// 擬似的な重い処理 3 (1秒)
static async Task<string> FetchConfigAsync()
{
await Task.Delay(1000); // 1秒待機
Console.WriteLine("[完了] 設定ファイル取得");
return "Config:OK";
}
}
解説と技術的なポイント
1. 直列実行 vs 並列実行
もしこれを await InitializeDatabaseAsync(); await LoadCacheAsync(); ... と記述していた場合、合計時間は 3秒 + 2秒 + 1秒 = 6秒 かかります。 Task.WhenAll を使うことで、これらは同時に走るため、ボトルネックである 3秒 +α の時間で全処理が完了します。
2. 戻り値の受け取り
Task.WhenAll<TResult> は、待機完了後に TResult[](結果の配列)を返します。配列内の順序は、WhenAll に渡したタスクの順序と一致するため、結果をインデックスで安全に取り出すことができます。
// 結果を個別に受け取る例
string dbResult = results[0];
string cacheResult = results[1];
3. 例外の挙動
複数のタスクのうち、1つでも例外が発生すると、await Task.WhenAll(...) は例外をスローします。 もし複数のタスクで同時に例外が発生した場合、await は「最初の例外」のみを再スローします。全ての例外を確認したい場合は、Task オブジェクトの Exception プロパティ(AggregateException)を確認する必要があります。
4. WaitAll との違い
似た名前の Task.WaitAll メソッドがありますが、これはスレッドをブロックする同期メソッドです。UIフリーズの原因となるため、GUIアプリや高負荷なWebサーバー処理では、非同期である Task.WhenAll を使用するのがモダンな作法です。
