Understanding Task.WaitAll and Task.WhenAll in C# with real world example

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.

Next Post Previous Post
No Comment
Add Comment
comment url