Concurrency and Multithreading in C#: Advanced Level
In modern C# development, concurrency and multithreading are not just additional capabilities but fundamental tools for creating fast, responsive, and scalable applications. At an advanced level, we go beyond simply launching Task.Run and encounter the intricacies of schedulers, context synchronization, and patterns for working with shared data. In this article, we will break down key advanced concurrency concepts in C# (.NET) that will help you write efficient and safe code.
1. Task and ValueTask: In-depth Usage
The Task class is the foundation of asynchronous programming in C#. However, at an advanced level, it is important to understand the difference between Task and ValueTask, as well as manage the task lifecycle.
When to use ValueTask?
ValueTask is a structure that allows you to avoid allocating memory for a Task object when the result is already available synchronously. This is critical for high-performance scenarios, such as in caching methods.
public class CacheService{ private readonly Dictionary<string, string> _cache = new();
public async ValueTask<string> GetDataAsync(string key) { if (_cache.TryGetValue(key, out string value)) { // Result already exists, no Task created return value; }
// Asynchronous data retrieval string data = await FetchFromDatabaseAsync(key); _cache[key] = data; return data; }
private async Task<string> FetchFromDatabaseAsync(string key) { await Task.Delay(100); // Simulating delay return $"Data for {key}"; }}Advanced Error Handling and Cancellation
Use CancellationToken for proper cancellation of long-running operations. Combine multiple tokens via CancellationTokenSource.CreateLinkedTokenSource.
public async Task ProcessWithTimeoutAsync(CancellationToken externalToken){ using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalToken, timeoutCts.Token);
try { await LongRunningOperationAsync(linkedCts.Token); } catch (OperationCanceledException) { Console.WriteLine("Operation cancelled or timeout exceeded."); }}2. SynchronizationContext and Task Schedulers
Understanding SynchronizationContext is necessary for working with UI threads (WPF, WinForms) and ASP.NET Core. At an advanced level, you can create your own synchronization contexts.
Configuring Context for the Thread Pool
By default, await captures the current SynchronizationContext. To avoid this (and prevent deadlocks), use ConfigureAwait(false).
public async Task ProcessDataAsync(){ // This code runs in the original context (e.g., UI) await Task.Run(() => HeavyComputation()).ConfigureAwait(false);
// After ConfigureAwait(false) — in the thread pool context // No access to UI elements! Console.WriteLine("Executed in a thread pool thread");}Creating a Custom Scheduler
You can implement your own TaskScheduler, for example, to limit the number of concurrently executing tasks (similar to SemaphoreSlim at the scheduler level).
public class LimitedConcurrencyTaskScheduler : TaskScheduler{ private readonly SemaphoreSlim _semaphore; private int _runningTasks;
public LimitedConcurrencyTaskScheduler(int maxConcurrency) { _semaphore = new SemaphoreSlim(maxConcurrency);