C# Concurrency and Parallelism

Welcome to the comprehensive guide on concurrency and parallelism in C#. Understanding these concepts is crucial for building responsive, scalable, and high-performance applications in modern software development.

Introduction to Concurrency and Parallelism

While often used interchangeably, concurrency and parallelism are distinct but related concepts:

C# provides powerful tools and abstractions to tackle both concurrency and parallelism effectively.

Key Concepts and Technologies

Threads

Threads are the fundamental building blocks of concurrency. A thread is the smallest unit of execution within a process. C# allows you to create and manage threads using the System.Threading.Thread class.


using System;
using System.Threading;

public class ThreadExample
{
    public static void PrintNumbers()
    {
        for (int i = 1; i <= 5; i++)
        {
            Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: {i}");
            Thread.Sleep(100); // Simulate work
        }
    }

    public static void Main(string[] args)
    {
        Thread thread1 = new Thread(PrintNumbers);
        Thread thread2 = new Thread(PrintNumbers);

        thread1.Start();
        thread2.Start();

        thread1.Join(); // Wait for thread1 to finish
        thread2.Join(); // Wait for thread2 to finish

        Console.WriteLine("All threads finished.");
    }
}
            

Task Parallel Library (TPL)

The Task Parallel Library (TPL) is a set of public types and APIs in the .NET Framework that enable developers to easily express and manage parallel operations. TPL simplifies writing multithreaded and parallel code, abstracting away much of the complexity of thread management.

The core type in TPL is System.Threading.Tasks.Task and Task<TResult>. Tasks represent an asynchronous operation that may complete at some later time.


using System;
using System.Threading.Tasks;

public class TplExample
{
    public static void Main(string[] args)
    {
        // Create tasks that will run in parallel
        Task task1 = Task.Run(() =>
        {
            for (int i = 1; i <= 5; i++)
            {
                Console.WriteLine($"Task 1: {i}");
                Task.Delay(100).Wait(); // Simulate work asynchronously
            }
        });

        Task task2 = Task.Run(() =>
        {
            for (int i = 1; i <= 5; i++)
            {
                Console.WriteLine($"Task 2: {i}");
                Task.Delay(100).Wait(); // Simulate work asynchronously
            }
        });

        // Wait for both tasks to complete
        Task.WaitAll(task1, task2);

        Console.WriteLine("All tasks finished.");
    }
}
            

Parallel LINQ (PLINQ)

PLINQ extends Language Integrated Query (LINQ) to enable parallel execution of LINQ queries. It automatically partitions and processes data in parallel across multiple cores, significantly speeding up data-intensive queries.

Simply add the .AsParallel() extension method to your LINQ query.


using System;
using System.Linq;

public class PlinqExample
{
    public static void Main(string[] args)
    {
        var numbers = Enumerable.Range(1, 1000000);

        // Parallel query execution
        var parallelQuery = numbers.AsParallel().Select(x => x * x);

        // Sequential query execution for comparison
        // var sequentialQuery = numbers.Select(x => x * x);

        // Force evaluation (e.g., by counting)
        int count = parallelQuery.Count();
        Console.WriteLine($"Count of squares: {count}");

        Console.WriteLine("PLINQ query completed.");
    }
}
            

Asynchronous Programming (async/await)

While not strictly parallelism, asynchronous programming is key to efficient concurrency. It allows your application to remain responsive while performing long-running operations (like I/O) by not blocking the main thread. The async and await keywords simplify writing asynchronous code.


using System;
using System.Net.Http;
using System.Threading.Tasks;

public class AsyncExample
{
    static readonly HttpClient client = new HttpClient();

    public static async Task DownloadPageAsync(string url)
    {
        Console.WriteLine($"Starting download for: {url}");
        try
        {
            string result = await client.GetStringAsync(url);
            Console.WriteLine($"Download complete for: {url}. Length: {result.Length}");
        }
        catch (HttpRequestException e)
        {
            Console.WriteLine($"Error downloading {url}: {e.Message}");
        }
    }

    public static async Task Main(string[] args)
    {
        Console.WriteLine("Starting asynchronous downloads...");
        Task download1 = DownloadPageAsync("https://www.microsoft.com");
        Task download2 = DownloadPageAsync("https://www.dotnet.org");

        await Task.WhenAll(download1, download2);

        Console.WriteLine("All downloads finished.");
    }
}
            

Synchronization Primitives

When multiple threads access shared data, race conditions and data corruption can occur. Synchronization primitives are used to manage access to shared resources and ensure thread safety.

Best Practices