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:
- Concurrency: The ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order, without affecting the final outcome. It's about managing multiple tasks that might be making progress at the same time.
- Parallelism: The ability to execute multiple tasks simultaneously. This typically requires multiple processing units (e.g., CPU cores).
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.
lockstatement: Ensures that a block of code is executed by only one thread at a time.Mutex,Semaphore,Monitor: More advanced synchronization mechanisms.Interlockedclass: Provides atomic operations for simple data types, useful for incrementing/decrementing counters without explicit locks.
Best Practices
- Minimize shared mutable state: The less data shared between threads, the fewer synchronization issues you'll encounter.
- Use TPL and async/await: Prefer these higher-level abstractions over manual thread management when possible.
- Avoid deadlocks: Carefully design locking strategies to prevent situations where threads are blocked indefinitely waiting for each other.
- Handle exceptions properly: Ensure that exceptions in background tasks or threads are caught and handled gracefully.
- Consider thread pools: The .NET runtime uses thread pools to manage threads efficiently, reducing overhead.