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.