Running Two Async Tasks in Parallel and Collecting Results in .NET 4.5

Dec 01, 2025 · Programming · 13 views · 7.8

Keywords: asynchronous programming | parallel execution | Task.WhenAll

Abstract: This article provides an in-depth exploration of how to leverage the async/await pattern in .NET 4.5 to execute multiple asynchronous tasks in parallel and efficiently collect their results. By comparing traditional Task.Run approaches with modern async/await techniques, it analyzes the differences between Task.Delay and Thread.Sleep, and demonstrates the correct implementation using Task.WhenAll to await multiple task completions. The discussion covers common pitfalls in asynchronous programming, such as the impact of blocking calls on parallelism, and offers complete code examples and best practices to help developers maximize the performance benefits of C# 4.5's asynchronous features.

Fundamentals of Asynchronous Programming and Parallel Execution Requirements

In .NET 4.5, the introduction of the async/await keywords revolutionized asynchronous programming, making it more intuitive and efficient to write non-blocking code. When multiple long-running tasks need to be executed simultaneously, parallel execution can significantly enhance application responsiveness and throughput. While traditional multithreading can achieve parallelism, it often leads to complex and error-prone code. The async/await pattern, through the Task-based Asynchronous Pattern (TAP), offers a cleaner solution.

Limitations of Traditional Approaches

In earlier implementations, developers commonly used Task.Run to wrap synchronous methods for parallel execution. For example:

public static void Go()
{
    Console.WriteLine("Starting");
    var task1 = Task.Run(() => Sleep(5000));    
    var task2 = Task.Run(() => Sleep(3000));
    int totalSlept = task1.Result + task2.Result;
    Console.WriteLine("Slept for a total of " + totalSlept + " ms");
}

private static int Sleep(int ms)
{
    Console.WriteLine("Sleeping for " + ms);
    Thread.Sleep(ms);
    Console.WriteLine("Sleeping for " + ms + " FINISHED");
    return ms;
}

Although this method achieves parallel execution, it has several drawbacks: First, the Sleep method is synchronous, and using Thread.Sleep blocks threads, failing to utilize asynchronous advantages fully. Second, the code structure is not elegant, requiring explicit calls to Task.Run. Finally, accessing results via task.Result may cause blocking, contradicting the purpose of asynchronous programming.

Correct Implementation with async/await Pattern

To fully leverage .NET 4.5's asynchronous features, methods should be marked as async and use Task.Delay instead of Thread.Sleep. Task.Delay returns a task that completes after a specified time, without blocking the thread, allowing other tasks to proceed. Here is the improved implementation:

public static async void GoAsync()
{
    Console.WriteLine("Starting");
    var task1 = Sleep(5000);
    var task2 = Sleep(3000);
    int[] result = await Task.WhenAll(task1, task2);
    Console.WriteLine("Slept for a total of " + result.Sum() + " ms");
}

private async static Task<int> Sleep(int ms)
{
    Console.WriteLine("Sleeping for {0} at {1}", ms, Environment.TickCount);
    await Task.Delay(ms);
    Console.WriteLine("Sleeping for {0} finished at {1}", ms, Environment.TickCount);
    return ms;
}

In this implementation, the Sleep method is defined as async Task<int>, internally using await Task.Delay(ms) for non-blocking delays. When Sleep(5000) and Sleep(3000) are called, both tasks start immediately and execute in parallel. The Task.WhenAll method awaits all tasks to complete and returns an array containing all results, with result.Sum() conveniently calculating the total.

Key Concept Analysis

Task.Delay vs Thread.Sleep: Task.Delay is asynchronous; it creates a task that completes after a specified time without blocking the current thread, allowing thread pool threads to handle other tasks. In contrast, Thread.Sleep is synchronous and blocks the current thread, wasting thread resources. In asynchronous programming, always use Task.Delay for delays.

Using Task.WhenAll: Task.WhenAll accepts multiple tasks as parameters and returns a task that completes when all input tasks are finished. By using await Task.WhenAll(task1, task2), you can non-blockingly wait for all tasks to complete, then access results via task.Result or directly from the returned array. This approach avoids sequential execution issues caused by waiting for tasks individually.

Timing of Async Method Calls: When calling an async method, such as Sleep(5000), it immediately returns a Task<int> object without waiting for the method to finish executing. This allows multiple async methods to start quickly, enabling true parallel execution. If await is incorrectly used at startup, e.g., await Sleep(5000), execution becomes sequential, losing parallelism.

Common Mistakes and Solutions

A common mistake is attempting to use Thread.Sleep within an async method, which causes blocking and disrupts the asynchronous flow. For example:

private static async Task<int> Sleep(int ms)
{
    Console.WriteLine("Sleeping for " + ms);
    Thread.Sleep(ms);  // Error: blocking call
    return ms;
}

In this case, even though the method is marked async, Thread.Sleep still blocks the thread, negating asynchronous benefits. The correct approach is to use await Task.Delay(ms).

Another error is accessing the Task.Result property too early, which may lead to deadlocks or blocking. In asynchronous contexts, use await to wait for task completion before accessing results. For instance, after awaiting all tasks with await Task.WhenAll, process the result array.

Performance and Best Practices

Using async/await for parallel task execution can significantly improve performance for I/O-intensive or high-latency operations. By avoiding thread blocking, applications utilize system resources more efficiently, enhancing responsiveness. In practice, follow these best practices:

  1. Mark long-running methods as async and return Task or Task<T>.
  2. Use Task.Delay instead of Thread.Sleep for non-blocking delays.
  3. Await multiple tasks in parallel with Task.WhenAll, avoiding sequential waits.
  4. Avoid synchronous blocking calls within async methods to maintain asynchronous flow integrity.
  5. Handle exceptions appropriately by wrapping await calls in try-catch or checking the Task.Exception property.

By mastering these core concepts and practices, developers can fully leverage .NET 4.5's asynchronous features to write efficient and maintainable parallel code.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.