Task Parallelism

Vaibhav • September 11, 2025

In the previous article, we explored how cancellation tokens allow asynchronous operations to be stopped gracefully. Now, we turn our attention to a powerful concept that builds on everything we've learned so far: task parallelism. This technique lets you run multiple tasks concurrently, making your applications faster and more responsive-especially when dealing with independent workloads.

In this article, we’ll explore how to create and manage parallel tasks using the Task class, how to coordinate their results, and how to handle exceptions and cancellation across multiple tasks. We’ll also look at performance considerations and best practices for writing scalable parallel code in C#.

What Is Task Parallelism?

Task parallelism is the ability to execute multiple tasks at the same time. These tasks may perform different operations and are often independent of each other. Unlike traditional threading, task parallelism in C# is built on the Task class, which abstracts away low-level thread management and provides a clean, composable API.

You’ve already seen how to run a single task using Task.Run. Now imagine running several tasks in parallel and waiting for all of them to complete. That’s task parallelism in action.


Task t1 = Task.Run(() => Console.WriteLine("Task 1"));
Task t2 = Task.Run(() => Console.WriteLine("Task 2"));
Task t3 = Task.Run(() => Console.WriteLine("Task 3"));

await Task.WhenAll(t1, t2, t3);
Console.WriteLine("All tasks completed");
    

This code starts three tasks concurrently. Task.WhenAll waits for all of them to finish before continuing. The tasks run in parallel, not sequentially.

Creating Parallel Tasks

You can create tasks using Task.Run, Task.Factory.StartNew, or by using async methods. Task.Run is the most common and recommended approach for starting background work.


Task t1 = Task.Run(() => Compute(1));
Task t2 = Task.Run(() => Compute(2));
Task t3 = Task.Run(() => Compute(3));

int[] results = await Task.WhenAll(t1, t2, t3);
Console.WriteLine($"Sum: {results.Sum()}");

int Compute(int value)
{
    Thread.Sleep(500);
    return value * 10;
}
    

Each task runs the Compute method with a different input. The results are collected using Task.WhenAll, and then summed. This pattern is useful when you need to process multiple items in parallel and combine the results.

Using async Methods in Parallel

You can also run async methods in parallel. Just start them without awaiting immediately, then use Task.WhenAll to wait for all of them.


Task t1 = FetchDataAsync("https://api.example.com/1");
Task t2 = FetchDataAsync("https://api.example.com/2");

string[] responses = await Task.WhenAll(t1, t2);
Console.WriteLine("Responses received");

async Task FetchDataAsync(string url)
{
    await Task.Delay(1000); // Simulate network delay
    return $"Data from {url}";
}
    

This approach is ideal for I/O-bound operations like web requests, file reads, or database queries. It allows you to maximize throughput without blocking threads.

Waiting for Any Task

Sometimes you want to proceed as soon as any one of several tasks completes. Use Task.WhenAny for that.


Task t1 = FetchDataAsync("A");
Task t2 = FetchDataAsync("B");

Task first = await Task.WhenAny(t1, t2);
Console.WriteLine("First response: " + await first);
    

This code starts two tasks and continues as soon as the first one finishes. You can then await the completed task to get its result.

Handling Exceptions in Parallel Tasks

If one or more tasks throw exceptions, Task.WhenAll will throw an AggregateException. You can catch it and inspect the inner exceptions.


Task t1 = Task.Run(() => throw new InvalidOperationException("Error 1"));
Task t2 = Task.Run(() => throw new ArgumentException("Error 2"));

try
{
    await Task.WhenAll(t1, t2);
}
catch (Exception ex)
{
    Console.WriteLine("Caught: " + ex.Message);
}
    

Only the first exception is rethrown by await. To access all exceptions, use the Exception property of the task.


foreach (var inner in t1.Exception.InnerExceptions)
{
    Console.WriteLine(inner.Message);
}
    

This is useful for logging and diagnostics when multiple tasks may fail independently.

Cancellation Across Multiple Tasks

You can cancel multiple tasks using a shared CancellationToken. Pass the token to each task and check it inside the task body.


CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task t1 = Task.Run(() => DoWork(token), token);
Task t2 = Task.Run(() => DoWork(token), token);

cts.CancelAfter(1000);

try
{
    await Task.WhenAll(t1, t2);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Tasks cancelled");
}

void DoWork(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        Thread.Sleep(200);
    }
}
    

This pattern ensures that all tasks respond to cancellation and exit cleanly. It also avoids wasting resources on unnecessary work.

Controlling Degree of Parallelism

Running too many tasks at once can overwhelm the system. You can control the degree of parallelism using throttling techniques or by using Parallel.ForEachAsync in .NET 6+.


await Parallel.ForEachAsync(urls, new ParallelOptions
{
    MaxDegreeOfParallelism = 4
}, async (url, token) =>
{
    string data = await FetchDataAsync(url);
    Console.WriteLine(data);
});
    

This code processes a list of URLs with a maximum of 4 concurrent tasks. It’s a clean and efficient way to manage parallel workloads.

Performance Considerations

Task parallelism improves performance by utilizing multiple cores and avoiding idle time. But it’s not free. Each task has overhead, and excessive parallelism can lead to contention, memory pressure, and thread pool exhaustion.

Measure performance using profiling tools and adjust the degree of parallelism based on workload and system capacity. Prefer async I/O over CPU-bound parallelism when possible.

Use Task.WhenAll for independent tasks, Task.WhenAny for race conditions, and Parallel.ForEachAsync for controlled concurrency. Always measure and tune for your specific scenario.

Summary

Task parallelism is a powerful technique for building fast and scalable applications. It allows you to run multiple tasks concurrently, coordinate their results, and handle exceptions and cancellation gracefully. You’ve learned how to create parallel tasks, use Task.WhenAll and Task.WhenAny, manage cancellation, and control concurrency.

In the next article, we’ll explore Concurrent Collections, which are designed to work safely in multi-threaded environments. You’ll learn how to use thread-safe data structures like ConcurrentDictionary and ConcurrentQueue to share data across tasks without risking race conditions.