In asynchronous programming, executing multiple independent processes (e.g., fetching data from both API A and API B) sequentially by await-ing them one by one is inefficient because the processing times add up.
Using the Task.WhenAll method allows you to start multiple tasks simultaneously (in parallel) and efficiently control the flow by “waiting until all tasks are complete.” This reduces the total processing time to the duration of the “slowest task.”
Implementing Parallel Execution with Task.WhenAll
The following sample code simulates a scenario where three different initialization processes (Database Connection, Cache Loading, Configuration Fetching) are executed in parallel.
We use System.Diagnostics.Stopwatch to verify how much time is saved compared to sequential execution.
Sample Code
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
public class Program
{
public static async Task Main()
{
Console.WriteLine("Starting application initialization...");
var stopwatch = Stopwatch.StartNew();
// 1. Define multiple tasks (Execution starts at this point)
// * Note: We do not await them here yet
Task<string> dbTask = InitializeDatabaseAsync();
Task<string> cacheTask = LoadCacheAsync();
Task<string> configTask = FetchConfigAsync();
// Create a list of tasks
var allTasks = new List<Task<string>> { dbTask, cacheTask, configTask };
try
{
Console.WriteLine("--- Running all tasks in parallel ---");
// 2. Wait for all tasks to complete using Task.WhenAll
// The return value is an array containing the results (string) of each task
string[] results = await Task.WhenAll(allTasks);
stopwatch.Stop();
Console.WriteLine("\n--- All processes completed ---");
Console.WriteLine($"Elapsed time: {stopwatch.Elapsed.TotalSeconds:F2} seconds");
// Theoretically, it should finish in a time close to the longest task (3 seconds)
// (Sequential execution would take 3+2+1 = 6 seconds)
// Display results
foreach (var result in results)
{
Console.WriteLine($"Result: {result}");
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
// Simulated heavy process 1 (3 seconds)
static async Task<string> InitializeDatabaseAsync()
{
await Task.Delay(3000); // Wait 3 seconds
Console.WriteLine("[Done] Database Connection");
return "DB:OK";
}
// Simulated heavy process 2 (2 seconds)
static async Task<string> LoadCacheAsync()
{
await Task.Delay(2000); // Wait 2 seconds
Console.WriteLine("[Done] Cache Loading");
return "Cache:OK";
}
// Simulated heavy process 3 (1 second)
static async Task<string> FetchConfigAsync()
{
await Task.Delay(1000); // Wait 1 second
Console.WriteLine("[Done] Config Fetching");
return "Config:OK";
}
}
Explanation and Technical Points
1. Sequential vs. Parallel Execution
If you wrote this as await InitializeDatabaseAsync(); await LoadCacheAsync(); ..., the total time would be 6 seconds (3s + 2s + 1s). By using Task.WhenAll, they run concurrently, so the entire process finishes in approximately 3 seconds (the duration of the longest task).
2. Receiving Return Values
Task.WhenAll<TResult> returns TResult[] (an array of results) after waiting is complete. The order of elements in the array matches the order of the tasks passed to WhenAll, so you can safely retrieve results by index.
// Example of receiving results individually
string dbResult = results[0];
string cacheResult = results[1];
3. Exception Behavior
If an exception occurs in any of the multiple tasks, await Task.WhenAll(...) will throw an exception. If multiple tasks throw exceptions simultaneously, await re-throws only the first exception. To check all exceptions, you need to inspect the Exception property (AggregateException) of the Task object.
4. Difference from WaitAll
There is a similar method called Task.WaitAll, but it is a synchronous method that blocks the thread. Since this can cause UI freezes, using the asynchronous Task.WhenAll is the modern standard for GUI applications and high-load web server processing.
