Keywords: C# | Asynchronous Programming | Task.WhenAll | Parallel Processing | await
Abstract: This article provides an in-depth exploration of how to await multiple tasks returning different types of results in C# asynchronous programming. Through the Task.WhenAll method, it demonstrates parallel task execution, analyzes differences between await and Task.Result, and offers complete code examples with exception handling strategies for writing efficient and reliable asynchronous code.
Core Concepts of Asynchronous Task Parallel Execution
In modern C# asynchronous programming, there is often a need to execute multiple independent tasks simultaneously and continue with subsequent logic after all tasks complete. When these tasks return results of different types, traditional sequential waiting approaches can reduce program efficiency. The Task.WhenAll method provides an elegant solution to this problem.
Basic Usage of Task.WhenAll
The Task.WhenAll method allows developers to await the completion of multiple tasks simultaneously, without concern for the specific types returned by these tasks. Here is a typical usage scenario:
private async Task<Cat> FeedCat() { }
private async Task<House> SellHouse() { }
private async Task<Tesla> BuyCar() { }
public async Task ExecuteAllTasks()
{
var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();
await Task.WhenAll(catTask, houseTask, carTask);
var cat = await catTask;
var house = await houseTask;
var car = await carTask;
}
Technical Analysis
In the above code, the three asynchronous methods begin execution immediately because asynchronous methods always return "hot" tasks (already started tasks). Task.WhenAll creates a new task that completes when all provided tasks have completed. This approach ensures maximum parallelism, as all tasks can run concurrently.
It is important to note that after Task.WhenAll completes, we can safely use await to retrieve each task's result because all tasks have successfully completed at this point. This approach is safer than directly using Task.Result, as Task.Result can cause deadlocks in certain scenarios.
Comparison with Sequential Waiting
Another common approach is to await each task sequentially:
var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();
var cat = await catTask;
var house = await houseTask;
var car = await carTask;
While this method achieves the same results, its execution efficiency may be inferior to Task.WhenAll. In sequential waiting, if the first task takes a long time, subsequent tasks, although started, cannot fully utilize parallel execution advantages as the program blocks at the first await.
Exception Handling Strategy
When using Task.WhenAll, if any task throws an exception, Task.WhenAll wraps these exceptions in an AggregateException. Developers need to handle this situation appropriately:
try
{
await Task.WhenAll(catTask, houseTask, carTask);
var cat = await catTask;
var house = await houseTask;
var car = await carTask;
}
catch (AggregateException ex)
{
// Handle exceptions that multiple tasks might throw
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"Task exception: {innerEx.Message}");
}
}
Performance Optimization Considerations
For tasks returning results of the same type, a more concise syntax can be used:
var results = await Task.WhenAll(
Task.Run(() => 10),
Task.Run(() => 20)
);
foreach (var result in results)
{
Console.WriteLine(result);
}
This syntax not only makes the code more concise but may also offer better performance in certain scenarios by avoiding multiple await operations.
Practical Application Scenarios
In real-world development, this pattern is particularly useful for scenarios requiring data retrieval from multiple sources, such as calling multiple APIs concurrently, executing parallel database queries, or processing multiple files simultaneously. By executing these operations in parallel, application response performance can be significantly improved.
Best Practice Recommendations
1. Always prefer using await over Task.Result to retrieve task results
2. Properly handle AggregateException to ensure program robustness
3. Consider using CancellationToken to support task cancellation
4. Evaluate the actual differences between sequential and parallel waiting in performance-sensitive scenarios
By appropriately utilizing Task.WhenAll, developers can write asynchronous code that is both efficient and reliable, fully leveraging the parallel processing capabilities of modern hardware.