C# Story

Chapter #13 – Threads Library

System.Threading: threads, tasks, locks, cancellation, concurrent collections

13.0 Prologue

System.Threading and the Task Parallel Library (TPL) provide everything needed for concurrent and parallel programming in C#. This chapter covers raw Thread objects, the ThreadPool, Task and Task<T> as the preferred high-level abstraction, mutual-exclusion primitives, cooperative cancellation, lock-free operations, and the thread-safe collection types in System.Collections.Concurrent.

13.1 Thread

System.Threading.Thread is the lowest-level concurrency primitive. Each thread runs a ThreadStart or ParameterizedThreadStart delegate: var t = new Thread(() => { Console.WriteLine($"worker on thread {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(500); }); t.IsBackground = true; // don't block process exit t.Priority = ThreadPriority.BelowNormal; t.Start(); t.Join(); // wait for completion Key static members: Thread.Sleep(ms), Thread.Yield(), Thread.CurrentThread, Thread.CurrentThread.ManagedThreadId. Foreground threads keep the process alive; background threads do not. Prefer Task over raw threads for most work. Use Thread only when you need fine-grained control over priority, STA apartment state (COM interop), or a dedicated long-running thread.

13.2 Task and Task<T>

Task represents an asynchronous operation. Task.Run queues work to the ThreadPool: // fire and forget with result Task<int> task = Task.Run(() => Enumerable.Range(1, 1_000_000).Sum()); Console.WriteLine("working..."); int sum = await task; Console.WriteLine($"sum = {sum}"); // run multiple tasks in parallel, wait for all Task[] tasks = Enumerable.Range(0, 4) .Select(i => Task.Run(() => DoWork(i))) .ToArray(); await Task.WhenAll(tasks); // take the first result that arrives Task<string> winner = await Task.WhenAny(FetchFastAsync(), FetchSlowAsync()); Console.WriteLine(winner.Result); Task.Delay is the async equivalent of Thread.Sleep — it yields the thread rather than blocking it: await Task.Delay(TimeSpan.FromSeconds(1)); Use TaskCreationOptions.LongRunning when spawning tasks that occupy a thread for an extended period to avoid starving the pool.

13.3 lock and Monitor

The lock statement is syntactic sugar over Monitor.Enter and Monitor.Exit. Always lock on a private object field, never on this or a type: private readonly object _gate = new(); void Increment() { lock (_gate) { _count++; } } Monitor.Wait / Monitor.Pulse / Monitor.PulseAll implement condition-variable signaling inside a lock block. In .NET 9+ prefer System.Threading.Lock (a dedicated lock type) over locking on a plain object: private readonly Lock _lock = new(); void Update() { using (_lock.EnterScope()) { _value = Compute(); } }

13.4 Mutex, Semaphore, and SemaphoreSlim

Mutex is a system-level lock that can span processes. Use it to ensure only one instance of an application runs: using var mutex = new Mutex(initiallyOwned: false, "Global\\MyApp"); if (!mutex.WaitOne(TimeSpan.Zero)) return; // another instance is running try { RunApp(); } finally { mutex.ReleaseMutex(); } SemaphoreSlim limits concurrency to N simultaneous holders. Its WaitAsync overload integrates cleanly with async/await: private static readonly SemaphoreSlim _throttle = new(3); // max 3 concurrent async Task FetchAsync(string url) { await _throttle.WaitAsync(); try { /* do work */ } finally { _throttle.Release(); } }

13.5 CancellationToken

CancellationTokenSource produces a token that signals cancellation cooperatively. Pass the token into every async or long-running method: using var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(5)); // automatic timeout async Task ProcessAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { await DoStepAsync(ct); } ct.ThrowIfCancellationRequested(); // propagate as OperationCanceledException } try { await ProcessAsync(cts.Token); } catch (OperationCanceledException) { Console.WriteLine("cancelled"); } Link multiple tokens with CancellationTokenSource.CreateLinkedTokenSource to cancel when any one of them fires.

13.6 Interlocked and Volatile

Interlocked provides atomic read-modify-write operations without a lock: private int _counter; // atomic increment / decrement Interlocked.Increment(ref _counter); Interlocked.Decrement(ref _counter); // compare-and-swap: set to newVal only if current value equals comparand int old = Interlocked.CompareExchange(ref _counter, newVal: 10, comparand: 0); // atomic read of a long on 32-bit platforms long snapshot = Interlocked.Read(ref _largField); Volatile.Read / Volatile.Write (or the volatile keyword) prevent the compiler and CPU from reordering accesses to a shared flag. Use them for simple stop-flag patterns where a full lock would be excessive.

13.7 Concurrent Collections

System.Collections.Concurrent provides thread-safe collections that avoid coarse-grained locking:
Type Use case
ConcurrentDictionary<K,V>Thread-safe key-value store with atomic GetOrAdd, AddOrUpdate
ConcurrentQueue<T>FIFO producer-consumer queue; TryDequeue never blocks
ConcurrentStack<T>LIFO stack; TryPop never blocks
ConcurrentBag<T>Unordered, optimized for same-thread produce-and-consume
BlockingCollection<T>Bounded FIFO with blocking Take and Add for classic producer-consumer pipelines
// ConcurrentDictionary - atomic get-or-add var cache = new ConcurrentDictionary<string, int>(); int count = cache.GetOrAdd("hits", _ => 0); cache.AddOrUpdate("hits", 1, (_, old) => old + 1); // BlockingCollection - bounded producer/consumer using var queue = new BlockingCollection<string>(boundedCapacity: 100); // producer Task.Run(() => { foreach (string item in source) queue.Add(item); queue.CompleteAdding(); }); // consumer foreach (string item in queue.GetConsumingEnumerable()) Process(item);

13.8 Epilogue

This chapter surveyed System.Threading: raw Thread objects, the Task abstraction, mutual exclusion with lock and Monitor, bounded concurrency with SemaphoreSlim, cooperative cancellation, lock-free atomics, and concurrent collections. The next chapter examines some of the more advanced and interesting corners of C#.

13.9 References

Thread — Microsoft docs
Task — Microsoft docs
SemaphoreSlim — Microsoft docs
CancellationTokenSource — Microsoft docs
Thread-safe collections overview
Managed threading best practices