The continuous growth in computer hardware capabilities has led to a steady increase in the number of applications that rely on parallel and asynchronous programming techniques. One of the most popular programming languages for modern software development is C#. You may already be familiar with the basics of C#, but implementing parallel and asynchronous programming can help you take your projects to the next level.
In this comprehensive guide, we’ll explore the benefits of parallel and asynchronous programming, dive into the various tools and techniques that C# provides, and provide practical examples to enhance your understanding of these concepts. This guide is aimed at intermediate developers, but even if you’re a seasoned C# programmer, you may find new insights and techniques to improve your code.
1: Parallel Programming in C#
1.1 Understanding Parallelism
Parallelism is the process of executing multiple tasks or threads simultaneously, allowing your application to perform tasks more efficiently. Parallel programming techniques can help you to take full advantage of multi-core processors, which are now commonplace in modern computers.
1.2 The Task Parallel Library (TPL)
C# offers the Task Parallel Library (TPL), a set of classes and methods designed to make it easier to create, run, and manage parallel tasks. The TPL is built on top of the ThreadPool, which manages a pool of worker threads for executing tasks. The TPL abstracts away many of the complexities of working with threads directly, allowing you to focus on implementing parallel algorithms.
1.3 Parallel.ForEach
and Parallel.For
The Parallel.ForEach
and Parallel.For
methods are two of the most commonly used TPL methods. These methods allow you to perform a parallel loop, dividing the iterations of the loop among available cores. This can greatly improve the performance of CPU-bound tasks, such as processing large data sets or performing complex calculations.
1.4 Handling Exceptions in Parallel Code
One of the challenges of parallel programming is handling exceptions that occur within parallel tasks. The TPL provides the AggregateException class, which captures multiple exceptions that occur during parallel execution. By using try-catch blocks and the AggregateException class, you can ensure that your parallel code is robust and handles errors gracefully.
2: Asynchronous Programming with C#
2.1 Understanding Asynchrony
Asynchronous programming is a technique that allows you to perform time-consuming operations without blocking the execution of your application. This is particularly useful when dealing with I/O-bound operations, such as reading from a file or downloading data from the internet.
2.2 The async
and await
Keywords
C# introduced the async
and await
keywords in version 5.0 to simplify the process of writing asynchronous code. By marking a method with the async
keyword, you indicate that it will contain one or more await expressions. These expressions allow the method to yield control back to the caller, while the awaited operation is performed. This prevents the application from becoming unresponsive during lengthy operations.
2.3 Task-based Asynchronous Pattern (TAP)
The Task-based Asynchronous Pattern (TAP) is a recommended approach to asynchronous programming in C#. TAP relies on the Task and Task classes to represent ongoing work that may not have completed yet. By using TAP, you can write asynchronous code that is easier to read, maintain, and debug.
2.4 Combining Asynchronous and Parallel Programming
In some scenarios, you may need to combine parallel and asynchronous programming to achieve optimal performance. C# provides various tools and techniques to help you combine these approaches effectively. For example, you can use the Task.WhenAll method to await the completion of multiple asynchronous tasks in parallel.
3: Practical Examples and Tips
3.1 Example: Parallelizing a CPU-bound Operation
Consider a scenario where you need to process a large collection of data points and perform complex calculations on each of them. The following example demonstrates how to use the Parallel.ForEach
method to parallelize this CPU-bound operation:
List<DataPoint> dataPoints = GetDataPoints();
List<Result> results = new List<Result>();
Parallel.ForEach(dataPoints, dataPoint =>
{
Result result = PerformComplexCalculation(dataPoint);
lock (results)
{
results.Add(result);
}
});
Code language: C# (cs)
In this example, the Parallel.ForEach
method automatically distributes the workload across multiple cores, improving the overall performance of the operation.
3.2 Example: Asynchronous File I/O
When working with file I/O operations, asynchronous programming can help prevent your application from becoming unresponsive. The following example demonstrates how to read the contents of a file asynchronously using the StreamReader
class:
public async Task<string> ReadFileAsync(string filePath)
{
using (StreamReader reader = new StreamReader(filePath))
{
return await reader.ReadToEndAsync();
}
}
// Usage
string filePath = "example.txt";
string fileContents = await ReadFileAsync(filePath);
Code language: C# (cs)
By using the async and await keywords, you can perform file I/O operations without blocking the main thread of your application.
3.3 Tips for Writing Efficient Parallel and Asynchronous Code
- Choose the right technique: Evaluate whether your operation is CPU-bound or I/O-bound, and choose parallel or asynchronous programming accordingly.
- Use cancellation tokens: Pass CancellationToken instances to your parallel or asynchronous tasks to allow for graceful cancellation of ongoing operations.
- Limit the degree of parallelism: When using TPL methods, consider setting the MaxDegreeOfParallelism property to prevent oversubscription of resources.
- Handle exceptions properly: Ensure that you use try-catch blocks and the AggregateException class to handle exceptions in parallel and asynchronous code.
Example Exercise: Parallel Image Processing and Asynchronous Download
In this exercise, we’ll create a C# console application that downloads a set of images from the internet asynchronously and then processes them in parallel to apply a grayscale filter. We will be using the TPL and async-await features discussed in the previous sections.
Step 1: Create a new C# Console Application project
Create a new C# Console Application project in Visual Studio or your favorite C# development environment.
Step 2: Add the necessary NuGet packages
We will be using the SixLabors.ImageSharp library for image processing. Add the following NuGet package to your project:
SixLabors.ImageSharp
To add the NuGet package in Visual Studio, right-click on your project in the Solution Explorer and select “Manage NuGet Packages.” Click on the “Browse” tab, search for SixLabors.ImageSharp
, select it from the search results, and click “Install” to add it to your project.
In Visual Studio Code, you can use the terminal to add the package by running the following command:
dotnet add package SixLabors.ImageSharp
Code language: C# (cs)
Step 3: Implement the image download and processing methods
Add the following methods to your Program
class:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
public static async Task DownloadImageAsync(string url, string destination)
{
using HttpClient client = new HttpClient();
using HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
using Stream responseStream = await response.Content.ReadAsStreamAsync();
using FileStream fileStream = File.Create(destination);
await responseStream.CopyToAsync(fileStream);
}
public static void ProcessImageInParallel(string sourcePath, string destinationPath)
{
using Image<Rgba32> image = Image.Load<Rgba32>(sourcePath);
Parallel.For(0, image.Height, y =>
{
for (int x = 0; x < image.Width; x++)
{
Rgba32 pixel = image[x, y];
byte gray = (byte)((0.3 * pixel.R) + (0.59 * pixel.G) + (0.11 * pixel.B));
pixel.R = pixel.G = pixel.B = gray;
image[x, y] = pixel;
}
});
image.Save(destinationPath);
}
Code language: C# (cs)
The DownloadImageAsync
method takes a URL and a destination file path, downloads the image asynchronously, and saves it to the destination path. The ProcessImageInParallel
method takes a source image path and a destination path, loads the image, processes it in parallel using a grayscale filter, and then saves the processed image to the destination path.
Step 4: Implement the main application logic
Add the following code to your Main
method:
public static async Task Main(string[] args)
{
List<string> imageUrls = new List<string>
{
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
// Add more image URLs
};
int counter = 0;
List<Task> downloadTasks = new List<Task>();
foreach (string url in imageUrls)
{
string destination = $"image{++counter}.jpg";
downloadTasks.Add(DownloadImageAsync(url, destination));
}
// Wait for all images to be downloaded
await Task.WhenAll(downloadTasks);
List<Task> processingTasks = new List<Task>();
for (int i = 1; i <= counter; i++)
{
string sourcePath = $"image{i}.jpg";
string destinationPath = $"image{i}_grayscale.jpg";
processingTasks.Add(Task.Run(() => ProcessImageInParallel(sourcePath, destinationPath)));
}
// Wait for all images to be processed
await Task.WhenAll(processingTasks);
Console.WriteLine("All images downloaded and processed successfully!");
}
Code language: C# (cs)
This code initializes a list of image URLs, downloads them asynchronously, and processes them in parallel to apply a grayscale filter. After downloading and processing all images, it prints a success message.
First, we iterate through the list of image URLs and call the DownloadImageAsync
method for each URL, adding the resulting tasks to a downloadTasks
list. Then, we use Task.WhenAll
to wait for all download tasks to complete.
After downloading all the images, we loop through the downloaded images and call the ProcessImageInParallel
method for each image. We wrap this method call inside a Task.Run
to execute the processing in parallel and add the resulting tasks to a processingTasks
list. Finally, we use Task.WhenAll
to wait for all processing tasks to complete.
Step 5: Test the application
To test the application, replace the example URLs in the imageUrls
list with valid image URLs. Run the application, and observe that it downloads and processes the images asynchronously and in parallel, creating new grayscale images in the application’s working directory.
This exercise demonstrates how to combine parallel and asynchronous programming techniques in C# to download and process images efficiently. By using the TPL and async-await features, you can create high-performance applications that take full advantage of modern hardware and provide a responsive user experience.