Skip to main content

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:

  1. The Task and Task<TResult> classes
  2. The Parallel class with methods like For, ForEach, and Invoke
  3. PLINQ (Parallel LINQ)
  4. Concurrent collection classes
  5. Low-level synchronization primitives

Getting Started with Tasks

Creating and Running Basic Tasks

A Task represents an asynchronous operation. Creating a task is straightforward:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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:

csharp
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:

csharp
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

  1. 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.

  2. Avoid blocking operations: Try not to use Task.Wait() or .Result as they can lead to deadlocks. Use await instead when possible.

  3. Handle exceptions properly: Always catch exceptions from parallel operations to prevent them from crashing your application.

  4. Be aware of shared state: When tasks run in parallel, be careful with shared data. Use thread-safe collections or synchronization mechanisms.

  5. Consider the overhead: For small operations, the overhead of creating tasks might outweigh the benefits of parallelism.

  6. Test with different hardware: Performance can vary significantly on different machines with different numbers of cores.

  7. 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 and Parallel.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

Exercises

  1. Create a program that calculates prime numbers in parallel using the Task Parallel Library.
  2. Implement a parallel image processing application that applies filters to multiple images simultaneously.
  3. Write a program that downloads multiple files in parallel with progress reporting and cancellation support.
  4. Modify the web scraper example to extract and count specific HTML tags from each downloaded page.
  5. 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! :)