Synchronous vs Asynchronous: Task.WaitAll and Task.WhenAll in .NET

In C#, asynchronous programming often involves running multiple tasks concurrently. Two common methods for handling multiple tasks are Task.WaitAll and Task.WhenAll. While they may seem similar, they serve different purposes and are used in different scenarios. This article explores the differences between Task.WaitAll and Task.WhenAll, along with practical examples to illustrate their usage.

What is Task.WaitAll?

Task.WaitAll is a synchronous method that blocks the calling thread until all the provided tasks have been completed. It’s useful when you need to ensure that a set of tasks has finished before proceeding, but it does so in a blocking manner, which means the thread calls Task.WaitAll is occupied until all tasks are done.

Example Usage of Task.WaitAll

using System;
using System.Threading.Tasks;
class Program
{
    static void Main()
    {
        Task task1 = Task.Run(() => PerformTask(1));
        Task task2 = Task.Run(() => PerformTask(2));
        Task task3 = Task.Run(() => PerformTask(3));
        Task.WaitAll(task1, task2, task3); // Blocks until all tasks complete
        Console.WriteLine("All tasks completed.");
    }
    static void PerformTask(int taskId)
    {
        Console.WriteLine($"Task {taskId} starting.");
        Task.Delay(1000).Wait(); // Simulate work
        Console.WriteLine($"Task {taskId} completed.");
    }
}

In this example, Task.WaitAll blocks the main thread until all three tasks are complete.

What is Task.WhenAll?

Task.WhenAll is an asynchronous method that returns a single task that is completed when all the provided tasks have been completed. Unlike Task.WaitAll, it does not block the calling thread. Instead, it allows the calling code to continue executing asynchronously.

Example Usage of Task.WhenAll

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        Task task1 = Task.Run(() => PerformTask(1));
        Task task2 = Task.Run(() => PerformTask(2));
        Task task3 = Task.Run(() => PerformTask(3));
        await Task.WhenAll(task1, task2, task3); // Waits for all tasks to complete asynchronously
        Console.WriteLine("All tasks completed.");
    }
    static void PerformTask(int taskId)
    {
        Console.WriteLine($"Task {taskId} starting.");
        Task.Delay(1000).Wait(); // Simulate work
        Console.WriteLine($"Task {taskId} completed.");
    }
}

In this example, Task.WhenAll allows the main method to await the completion of all tasks without blocking the calling thread.

Key Differences

  1. Blocking vs. Non-blocking
    • Task.WaitAll: Blocks the calling thread until all tasks are complete.
    • Task.WhenAll: Returns a task that can be awaited, allowing the calling thread to continue execution asynchronously.
  2. Return Type
    • Task.WaitAll: Does not return a value.
    • Task.WhenAll: Returns a Task that represents the completion of all provided tasks.
  3. Usage Scenario
    • Task.WaitAll: Used when you need to block until tasks are complete, typically in non-UI or console applications.
    • Task.WhenAll: Used in asynchronous programming, especially in UI applications where blocking the main thread is undesirable.

Practical Use Cases

  1. When to Use Task.WaitAll
    • In console applications where you need to ensure that certain tasks are completed before moving on.
    • When you are dealing with legacy code that doesn’t support async/await patterns.
  2. When to Use Task.WhenAll
    • In UI applications keep the interface responsive.
    • In web applications handle multiple asynchronous operations without blocking the main thread.

Conclusion

Task.WaitAll and Task.WhenAll are essential tools in C# for handling multiple tasks. Use Task.WaitAll when you need to block the calling thread until tasks are complete, and Task.WhenAll for asynchronous waiting. Understanding their differences and appropriate use cases can help you write more efficient and responsive applications.