C# Task Parallel Library
Introduction
The Task Parallel Library (TPL) is a powerful set of APIs introduced in .NET Framework 4.0 that makes it easier to write concurrent and parallel code in C#. As applications need to handle more data and perform complex operations, leveraging multiple processor cores has become essential for creating responsive and high-performance software.
The TPL abstracts many of the complex details of thread management, scheduling, and coordination, allowing developers to focus on the business logic of their applications rather than the intricacies of parallel programming.
In this tutorial, you'll learn:
- What the Task Parallel Library is and its benefits
- How to create and manage tasks
- Techniques for parallel data processing
- How to handle exceptions in parallel code
- Best practices for using the TPL
What is the Task Parallel Library?
The Task Parallel Library is built around the concept of a Task
, which represents an asynchronous operation. Unlike traditional threading approaches, TPL provides a higher-level abstraction that simplifies:
- Creating and scheduling work
- Managing task dependencies
- Handling exceptions
- Canceling operations
- Monitoring task status
The TPL is composed of the following main components:
- The
Task
andTask<TResult>
classes - The
Parallel
class with methods likeFor
,ForEach
, andInvoke
- PLINQ (Parallel LINQ)
- Concurrent collection classes
- Low-level synchronization primitives
Getting Started with Tasks
Creating and Running Basic Tasks
A Task
represents an asynchronous operation. Creating a task is straightforward:
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Create and start a task
Task task = Task.Run(() => {
Console.WriteLine("Task is running");
// Do some work here
});
Console.WriteLine("Main thread continues working...");
// Wait for the task to complete
task.Wait();
Console.WriteLine("Task completed");
}
}
Output:
Main thread continues working...
Task is running
Task completed
Creating Tasks with Return Values
Tasks can also return values using the Task<TResult>
class:
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Create a task that returns a value
Task<int> calculateTask = Task.Run(() => {
Console.WriteLine("Calculating...");
// Simulate work by waiting
Task.Delay(1000).Wait();
return 42;
});
Console.WriteLine("Waiting for result...");
// Get the result (this will block until the task completes)
int result = calculateTask.Result;
Console.WriteLine($"The answer is: {result}");
}
}
Output:
Waiting for result...
Calculating...
The answer is: 42
Creating Tasks with TaskCompletionSource
Sometimes you need more control over when a task completes. That's where TaskCompletionSource<TResult>
helps:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
var tcs = new TaskCompletionSource<int>();
// Start a background operation
new Thread(() => {
Thread.Sleep(1000); // Simulate work
tcs.SetResult(100); // Complete the task with a result
}).Start();
Console.WriteLine("Waiting for the operation to complete...");
Task<int> task = tcs.Task; // Get the task
Console.WriteLine($"Result: {task.Result}");
}
}
Output:
Waiting for the operation to complete...
Result: 100
Parallel Data Processing
The TPL provides higher-level constructs for parallel operations through the Parallel
class.
Parallel.For
The Parallel.For
method is similar to a standard for loop but executes iterations in parallel:
using System;
using System.Threading.Tasks;
using System.Diagnostics;
class Program
{
static void Main()
{
const int ArraySize = 10000000;
double[] data = new double[ArraySize];
// Initialize the array
for (int i = 0; i < data.Length; i++)
{
data[i] = i;
}
Stopwatch stopwatch = new Stopwatch();
// Sequential processing
stopwatch.Start();
for (int i = 0; i < data.Length; i++)
{
data[i] = Math.Sqrt(data[i]);
}
stopwatch.Stop();
Console.WriteLine($"Sequential processing took: {stopwatch.ElapsedMilliseconds}ms");
// Reset data
for (int i = 0; i < data.Length; i++)
{
data[i] = i;
}
// Parallel processing
stopwatch.Restart();
Parallel.For(0, data.Length, i =>
{
data[i] = Math.Sqrt(data[i]);
});
stopwatch.Stop();
Console.WriteLine($"Parallel processing took: {stopwatch.ElapsedMilliseconds}ms");
}
}
Output (results will vary based on your hardware):
Sequential processing took: 58ms
Parallel processing took: 15ms
Parallel.ForEach
The Parallel.ForEach
method works with collections:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static void Main()
{
List<string> items = new List<string> {
"Item 1", "Item 2", "Item 3", "Item 4", "Item 5",
"Item 6", "Item 7", "Item 8", "Item 9", "Item 10"
};
Parallel.ForEach(items, item =>
{
ProcessItem(item);
});
Console.WriteLine("All items processed");
}
static void ProcessItem(string item)
{
Console.WriteLine($"Processing {item} on thread {Task.CurrentId ?? -1}");
// Simulate variable processing time
Task.Delay(100 * new Random().Next(1, 10)).Wait();
}
}
Output (the order will vary):
Processing Item 1 on thread 1
Processing Item 3 on thread 3
Processing Item 2 on thread 2
Processing Item 4 on thread 4
Processing Item 6 on thread 4
Processing Item 5 on thread 1
Processing Item 8 on thread 2
Processing Item 7 on thread 3
Processing Item 9 on thread 1
Processing Item 10 on thread 3
All items processed
Parallel.Invoke
The Parallel.Invoke
method runs multiple actions in parallel:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Console.WriteLine("Starting multiple operations in parallel");
Parallel.Invoke(
() => ProcessFiles(),
() => ProcessNetwork(),
() => ProcessDatabase()
);
Console.WriteLine("All operations completed");
}
static void ProcessFiles()
{
Console.WriteLine("Starting file processing...");
Thread.Sleep(1000); // Simulate work
Console.WriteLine("File processing complete");
}
static void ProcessNetwork()
{
Console.WriteLine("Starting network operations...");
Thread.Sleep(2000); // Simulate work
Console.WriteLine("Network operations complete");
}
static void ProcessDatabase()
{
Console.WriteLine("Starting database operations...");
Thread.Sleep(1500); // Simulate work
Console.WriteLine("Database operations complete");
}
}
Output (order may vary):
Starting multiple operations in parallel
Starting file processing...
Starting network operations...
Starting database operations...
File processing complete
Database operations complete
Network operations complete
All operations completed
Task Continuations
Continuations allow you to specify what happens after a task completes, enabling you to create task chains.
Basic Continuations
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Task firstTask = Task.Run(() => {
Console.WriteLine("First task starting");
Task.Delay(1000).Wait();
Console.WriteLine("First task completed");
});
Task secondTask = firstTask.ContinueWith(task => {
Console.WriteLine("Second task starting");
Task.Delay(1000).Wait();
Console.WriteLine("Second task completed");
});
Console.WriteLine("Main thread waiting for tasks to complete");
secondTask.Wait();
Console.WriteLine("All tasks completed");
}
}
Output:
Main thread waiting for tasks to complete
First task starting
First task completed
Second task starting
Second task completed
All tasks completed
Continuation with Result
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Task<int> calculationTask = Task.Run(() => {
Console.WriteLine("Calculating first value");
Task.Delay(1000).Wait();
return 42;
});
Task<string> formatTask = calculationTask.ContinueWith(task => {
int result = task.Result;
Console.WriteLine($"Formatting result: {result}");
return $"The answer is {result}";
});
Console.WriteLine("Main thread waiting for result");
string formattedResult = formatTask.Result;
Console.WriteLine(formattedResult);
}
}
Output:
Main thread waiting for result
Calculating first value
Formatting result: 42
The answer is 42
Error Handling in Parallel Code
Error handling in parallel code requires special attention, as exceptions don't propagate automatically to the calling thread.
Handling Exceptions in Tasks
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Task task = Task.Run(() => {
Console.WriteLine("Task starting");
throw new InvalidOperationException("Something went wrong");
});
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
}
Console.WriteLine("Program continues after exception");
}
}
Output:
Task starting
Caught exception: Something went wrong
Program continues after exception
Handling Multiple Exceptions
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
var tasks = new Task[3];
tasks[0] = Task.Run(() => {
Console.WriteLine("Task 1 running");
// This task succeeds
});
tasks[1] = Task.Run(() => {
Console.WriteLine("Task 2 running");
throw new ArgumentException("Invalid argument");
});
tasks[2] = Task.Run(() => {
Console.WriteLine("Task 3 running");
throw new InvalidOperationException("Operation failed");
});
try
{
Task.WaitAll(tasks);
}
catch (AggregateException ae)
{
Console.WriteLine($"Caught {ae.InnerExceptions.Count} exceptions:");
foreach (var ex in ae.InnerExceptions)
{
Console.WriteLine($"- {ex.GetType().Name}: {ex.Message}");
}
}
Console.WriteLine("Program completed");
}
}
Output:
Task 1 running
Task 2 running
Task 3 running
Caught 2 exceptions:
- ArgumentException: Invalid argument
- InvalidOperationException: Operation failed
Program completed
Cancellation Support
The TPL includes built-in support for cancellation through CancellationToken
:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task task = Task.Run(() => {
for (int i = 0; i < 100; i++)
{
// Check for cancellation
token.ThrowIfCancellationRequested();
Console.WriteLine($"Working... {i}%");
Thread.Sleep(100);
}
Console.WriteLine("Work completed successfully");
}, token);
Console.WriteLine("Press Enter to cancel the operation");
Console.ReadLine();
try
{
cts.Cancel();
task.Wait();
}
catch (AggregateException ae)
{
if (ae.InnerExceptions[0] is OperationCanceledException)
{
Console.WriteLine("Operation was canceled");
}
else
{
Console.WriteLine($"Error: {ae.InnerException.Message}");
}
}
Console.WriteLine("Program completed");
}
}
Output (assuming the user presses Enter after a few iterations):
Press Enter to cancel the operation
Working... 0%
Working... 1%
Working... 2%
Working... 3%
Operation was canceled
Program completed
Real-World Example: Web Scraper
Let's create a practical example of a simple web scraper that downloads multiple web pages in parallel:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
List<string> urls = new List<string>
{
"https://www.example.com",
"https://www.microsoft.com",
"https://www.github.com",
"https://www.stackoverflow.com",
"https://www.cnn.com"
};
Stopwatch stopwatch = new Stopwatch();
// Sequential download
stopwatch.Start();
foreach (string url in urls)
{
await DownloadWebsiteContentAsync(url);
}
stopwatch.Stop();
Console.WriteLine($"Sequential download took: {stopwatch.ElapsedMilliseconds}ms");
// Parallel download
stopwatch.Restart();
List<Task> downloadTasks = new List<Task>();
foreach (string url in urls)
{
downloadTasks.Add(DownloadWebsiteContentAsync(url));
}
await Task.WhenAll(downloadTasks);
stopwatch.Stop();
Console.WriteLine($"Parallel download took: {stopwatch.ElapsedMilliseconds}ms");
}
static async Task DownloadWebsiteContentAsync(string url)
{
using (HttpClient client = new HttpClient())
{
try
{
Console.WriteLine($"Downloading content from {url}");
string content = await client.GetStringAsync(url);
Console.WriteLine($"Downloaded {content.Length} characters from {url}");
}
catch (Exception ex)
{
Console.WriteLine($"Error downloading {url}: {ex.Message}");
}
}
}
}
Output (times will vary based on your internet connection):
Downloading content from https://www.example.com
Downloaded 1256 characters from https://www.example.com
Downloading content from https://www.microsoft.com
Downloaded 42568 characters from https://www.microsoft.com
...
Sequential download took: 5237ms
Downloading content from https://www.example.com
Downloading content from https://www.microsoft.com
Downloading content from https://www.github.com
Downloading content from https://www.stackoverflow.com
Downloading content from https://www.cnn.com
Downloaded 1256 characters from https://www.example.com
Downloaded 42568 characters from https://www.microsoft.com
...
Parallel download took: 1854ms
Best Practices for Using TPL
-
Don't create too many tasks: Creating thousands of tiny tasks can lead to overhead. Aim for tasks that take at least a few milliseconds to complete.
-
Avoid blocking operations: Try not to use
Task.Wait()
or.Result
as they can lead to deadlocks. Useawait
instead when possible. -
Handle exceptions properly: Always catch exceptions from parallel operations to prevent them from crashing your application.
-
Be aware of shared state: When tasks run in parallel, be careful with shared data. Use thread-safe collections or synchronization mechanisms.
-
Consider the overhead: For small operations, the overhead of creating tasks might outweigh the benefits of parallelism.
-
Test with different hardware: Performance can vary significantly on different machines with different numbers of cores.
-
Implement cancellation support: Always allow users to cancel long-running operations when possible.
Summary
The Task Parallel Library is a powerful set of tools that help you write concurrent code that can take advantage of multi-core processors. In this tutorial, you learned:
- How to create and manage tasks
- How to use parallel loops with
Parallel.For
andParallel.ForEach
- How to run multiple operations concurrently with
Parallel.Invoke
- How to chain tasks using continuations
- How to handle exceptions in parallel code
- How to implement cancellation support
- How to apply TPL to real-world scenarios
By using the TPL, you can significantly improve the performance and responsiveness of your applications while writing cleaner and more maintainable code.
Additional Resources
- Microsoft Docs: Task Parallel Library
- Microsoft Docs: Parallel Programming in .NET
- GitHub: Parallel Patterns Library Samples
Exercises
- Create a program that calculates prime numbers in parallel using the Task Parallel Library.
- Implement a parallel image processing application that applies filters to multiple images simultaneously.
- Write a program that downloads multiple files in parallel with progress reporting and cancellation support.
- Modify the web scraper example to extract and count specific HTML tags from each downloaded page.
- Create a parallel sorting algorithm and compare its performance with sequential sorting.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)