15 minlesson

Task.WhenAll - Parallel Execution

Task.WhenAll - Parallel Execution

Task.WhenAll runs multiple tasks concurrently and waits for all of them to complete. It's essential for maximizing throughput when tasks are independent.

Basic Usage

csharp
1// Start multiple tasks
2Task<string> task1 = FetchFromApi1Async();
3Task<string> task2 = FetchFromApi2Async();
4Task<string> task3 = FetchFromApi3Async();
5
6// Wait for all to complete
7await Task.WhenAll(task1, task2, task3);
8
9// All tasks are now complete
10string result1 = task1.Result; // Safe - already complete
11string result2 = task2.Result;
12string result3 = task3.Result;

Getting Results Directly

When all tasks return the same type, WhenAll returns an array:

csharp
1Task<int>[] tasks = new[]
2{
3 ComputeAsync(1),
4 ComputeAsync(2),
5 ComputeAsync(3)
6};
7
8int[] results = await Task.WhenAll(tasks);
9// results = [10, 20, 30]
10
11Console.WriteLine($"Sum: {results.Sum()}");

Sequential vs Parallel Comparison

csharp
1// SEQUENTIAL - Takes 3 seconds
2var sw = Stopwatch.StartNew();
3var r1 = await FetchDataAsync("A"); // 1 second
4var r2 = await FetchDataAsync("B"); // 1 second
5var r3 = await FetchDataAsync("C"); // 1 second
6Console.WriteLine($"Sequential: {sw.ElapsedMilliseconds}ms"); // ~3000ms
7
8// PARALLEL - Takes 1 second
9sw.Restart();
10var t1 = FetchDataAsync("A"); // Start immediately
11var t2 = FetchDataAsync("B"); // Start immediately
12var t3 = FetchDataAsync("C"); // Start immediately
13await Task.WhenAll(t1, t2, t3); // Wait for all
14Console.WriteLine($"Parallel: {sw.ElapsedMilliseconds}ms"); // ~1000ms

Dynamic Number of Tasks

csharp
1List<string> urls = GetUrlsToFetch();
2
3// Create tasks dynamically
4var tasks = urls.Select(url => httpClient.GetStringAsync(url));
5
6// Wait for all
7string[] results = await Task.WhenAll(tasks);
8
9foreach (var result in results)
10{
11 ProcessResult(result);
12}

Handling Exceptions

When multiple tasks fail, WhenAll aggregates exceptions:

csharp
1var tasks = new[]
2{
3 FailingTaskAsync("Error 1"),
4 SucceedingTaskAsync(),
5 FailingTaskAsync("Error 2")
6};
7
8Task<int[]> allTask = Task.WhenAll(tasks);
9
10try
11{
12 await allTask;
13}
14catch (Exception ex)
15{
16 // Only first exception thrown by await
17 Console.WriteLine($"First error: {ex.Message}");
18
19 // Get ALL exceptions from the task
20 if (allTask.Exception != null)
21 {
22 foreach (var inner in allTask.Exception.InnerExceptions)
23 {
24 Console.WriteLine($"Error: {inner.Message}");
25 }
26 }
27}

Partial Success Pattern

Sometimes you want to process successful results even if some fail:

csharp
1var tasks = urls.Select(async url =>
2{
3 try
4 {
5 return (Url: url, Data: await FetchAsync(url), Error: null as Exception);
6 }
7 catch (Exception ex)
8 {
9 return (Url: url, Data: null as string, Error: ex);
10 }
11});
12
13var results = await Task.WhenAll(tasks);
14
15var succeeded = results.Where(r => r.Error == null);
16var failed = results.Where(r => r.Error != null);
17
18Console.WriteLine($"Succeeded: {succeeded.Count()}, Failed: {failed.Count()}");

WhenAll with Empty Collection

csharp
1var emptyTasks = new List<Task<int>>();
2int[] results = await Task.WhenAll(emptyTasks);
3// results is an empty array, no exception

WhenAll with void Tasks

For tasks that don't return values:

csharp
1Task[] tasks = new[]
2{
3 SendEmailAsync("user1@example.com"),
4 SendEmailAsync("user2@example.com"),
5 SendEmailAsync("user3@example.com")
6};
7
8await Task.WhenAll(tasks);
9// All emails sent

Performance Considerations

Good: Independent Operations

csharp
1// These are independent - good for WhenAll
2var userData = FetchUserAsync(userId);
3var orderData = FetchOrdersAsync(userId);
4var preferences = FetchPreferencesAsync(userId);
5
6await Task.WhenAll(userData, orderData, preferences);

Bad: Dependent Operations

csharp
1// DON'T use WhenAll for dependent operations
2var user = await GetUserAsync(id); // Need user first
3var orders = await GetOrdersAsync(user.Id); // Depends on user
4var details = await GetDetailsAsync(orders); // Depends on orders

Limiting Concurrency

WhenAll starts all tasks immediately. For rate limiting, see Topic 5 (Throttling).

csharp
1// WARNING: This starts 1000 concurrent requests!
2var tasks = urls.Select(url => httpClient.GetAsync(url));
3await Task.WhenAll(tasks); // May overwhelm the server

Key Takeaways

  • Task.WhenAll waits for all tasks to complete
  • Tasks run concurrently, not sequentially
  • Returns array of results for Task<T>[]
  • First exception is thrown; check .Exception for all errors
  • Use for independent operations only
  • Empty collections return empty arrays