Asynchronous programming in C# has become an essential part of developing responsive and scalable applications. When working with multiple asynchronous tasks, developers often need to synchronize their completion. In this guide, we’ll explore two common approaches for waiting on multiple tasks: Task.WaitAll
and Task.WhenAll
. We’ll delve into their differences, provide real-world examples, and discuss exception handling strategies.
Understanding Task.WaitAll
Task.WaitAll
is a synchronous method that blocks the calling thread until all provided tasks have completed. It’s a straightforward option when you need to ensure that multiple tasks finish before proceeding with the rest of the code.
Real-World Example:
Let’s simulate a scenario where we download multiple files concurrently. Each file download is represented by an asynchronous task. Using Task.WaitAll
ensures that the program waits for all downloads to complete before continuing.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
await DownloadFilesAsync();
}
public static async Task DownloadFilesAsync()
{
List<Task> downloadTasks = new List<Task>
{
DownloadFileAsync("file1"),
DownloadFileAsync("file2"),
DownloadFileAsync("file3")
};
try
{
// Wait for all tasks to complete
Task.WaitAll(downloadTasks.ToArray());
Console.WriteLine("All files downloaded successfully.");
}
catch (AggregateException ex)
{
// Handle exceptions thrown by any of the tasks
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"Download failed: {innerEx.Message}");
}
}
}
public static async Task DownloadFileAsync(string fileName)
{
// Simulate file download delay
await Task.Delay(1000);
Console.WriteLine($"{fileName} downloaded.");
// Simulate an exception during download
if (fileName == "file2")
{
throw new Exception("Download failed for file2.");
}
}
}
In this example, DownloadFileAsync
simulates a file download with a delay. The DownloadFilesAsync
method waits for all downloads using Task.WaitAll
and handles exceptions using AggregateException
.
Exploring Task.WhenAll
Task.WhenAll
is an asynchronous method that returns a task completing when all provided tasks have finished. Unlike Task.WaitAll
, it doesn’t block the calling thread, making it suitable for non-blocking scenarios.
Real-World Example:
Consider a scenario where you need to fetch data from multiple APIs concurrently. Each API call is represented by an asynchronous task. Using Task.WhenAll
allows you to asynchronously wait for the completion of all API calls without blocking the main thread.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
await GetDataFromApisAsync();
}
public static async Task<IEnumerable<Data>> GetDataFromApisAsync()
{
List<Task<IEnumerable<Data>>> apiTasks = new List<Task<IEnumerable<Data>>>
{
FetchDataFromApiAsync("api1"),
FetchDataFromApiAsync("api2"),
FetchDataFromApiAsync("api3")
};
try
{
// Await Task.WhenAll to asynchronously wait for all API calls to complete
IEnumerable<Data>[] results = await Task.WhenAll(apiTasks);
// Combine results and return
return results.SelectMany(result => result);
}
catch (Exception ex)
{
// Handle exceptions thrown by any of the tasks
Console.WriteLine($"Error fetching data: {ex.Message}");
return Enumerable.Empty<Data>();
}
}
public static async Task<IEnumerable<Data>> FetchDataFromApiAsync(string apiName)
{
// Simulate API call delay
await Task.Delay(1500);
Console.WriteLine($"Data fetched from {apiName}.");
// Simulate an exception during API call
if (apiName == "api2")
{
throw new Exception($"Error fetching data from {apiName}.");
}
return new List<Data> { new Data(apiName, 1), new Data(apiName, 2) };
}
public class Data
{
public string Source { get; }
public int Value { get; }
public Data(string source, int value)
{
Source = source;
Value = value;
}
}
}
In this example, FetchDataFromApiAsync
simulates fetching data from an API with a delay. The GetDataFromApisAsync
method uses Task.WhenAll
to asynchronously wait for the completion of all API calls and handles exceptions using a general Exception
catch block.
Exception Handling with Task.WaitAll and Task.WhenAll
Exception Handling with Task.WaitAll
When using Task.WaitAll
, exceptions are aggregated into an AggregateException
. If any of the tasks being waited on throw an exception, Task.WaitAll
will throw an AggregateException
containing all the individual exceptions. To handle these exceptions, you can use a try-catch block, catching the AggregateException
and examining its InnerExceptions
property.
try
{
// Wait for all tasks to complete
Task.WaitAll(downloadTasks.ToArray());
Console.WriteLine("All files downloaded successfully.");
}
catch (AggregateException ex)
{
// Handle exceptions thrown by any of the tasks
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"Download failed: {innerEx.Message}");
}
}
Exception Handling with Task.WhenAll
In contrast, Task.WhenAll
behaves differently regarding exceptions. If any of the tasks being awaited on throw an exception, Task.WhenAll
itself will not throw an exception. Instead, the exceptions are propagated through the resulting task’s status. You can inspect the exceptions using the await
keyword or by accessing the Exception
property of the individual tasks.
try
{
// Await Task.WhenAll to asynchronously wait for all API calls to complete
IEnumerable<Data>[] results = await Task.WhenAll(apiTasks);
// Combine results and return
return results.SelectMany(result => result);
}
catch (Exception ex)
{
// Handle exceptions thrown by any of the tasks
Console.WriteLine($"Error fetching data: {ex.Message}");
return Enumerable.Empty<Data>();
}
In the case of Task.WhenAll
, the resulting task itself won’t throw an exception due to any individual task failing. Instead, you need to inspect the individual tasks for exceptions.
Conclusion
Understanding how exceptions are handled is crucial when working with asynchronous tasks. While Task.WaitAll
aggregates exceptions into an AggregateException
, Task.WhenAll
allows exceptions to be inspected individually. Choose the approach that best suits your error-handling strategy and application requirements. Proper exception handling ensures that your asynchronous code remains robust and can gracefully recover from unexpected errors.