# Threading, the TPL and Synchronization

## 1. Threads, the Thread Pool and Tasks

Creating OS threads is expensive (about 1 MB of stack each, kernel scheduling). .NET keeps a **thread pool** of worker threads and reuses them; the **Task Parallel Library (TPL)** schedules `Task`s onto it. You almost never create `Thread` objects directly.

| Need | Tool |
|---|---|
| Wait for I/O without blocking | `async`/`await` |
| CPU-bound work in parallel over a collection | `Parallel.For/ForEach`, PLINQ (`AsParallel()`) |
| Bounded concurrent async work | `Parallel.ForEachAsync(items, new ParallelOptions { MaxDegreeOfParallelism = 8 }, ...)` |
| Offload CPU work from a UI thread | `Task.Run` |
| Producer/consumer pipelines | `System.Threading.Channels` |
| Long-running background loops in a service | `BackgroundService` (hosted service) |

The pool grows slowly when threads are blocked (roughly one new thread per second or so beyond the minimum), which is why blocking calls in ASP.NET Core cause **thread pool starvation**: requests queue while the pool slowly adds threads.

## 2. Race Conditions and Synchronization Primitives

Shared mutable state accessed by several threads needs synchronization:

- `lock (obj)` (Monitor): mutual exclusion for short critical sections; can't contain `await`. .NET 9 adds `System.Threading.Lock`.
- `Interlocked.Increment/CompareExchange`: atomic operations on single variables, lock-free.
- `SemaphoreSlim`: limit concurrency, and `WaitAsync` works as an async lock.
- `ReaderWriterLockSlim`: many readers or one writer.
- `volatile` / `Volatile.Read`: visibility only, not atomicity of compound operations.
- Concurrent collections: `ConcurrentDictionary`, `ConcurrentQueue`, `ConcurrentBag`, `BlockingCollection`.

The best synchronization is none: prefer **immutability**, message passing (channels), and confining state to one thread or one actor.

## 3. Deadlocks

Two threads each hold a lock the other needs. Avoid by acquiring locks in a **consistent global order** (e.g. by account ID), keeping critical sections small, never calling unknown code (events, callbacks) while holding a lock, and using timeouts (`Monitor.TryEnter`). Sync-over-async (`.Result`) is a deadlock source too.

## 4. Channels: Producer/Consumer

```csharp
var channel = Channel.CreateBounded<Order>(new BoundedChannelOptions(100) { FullMode = BoundedChannelFullMode.Wait });
// producer
await channel.Writer.WriteAsync(order, ct);   // waits when full: backpressure
channel.Writer.Complete();
// consumers
await foreach (var o in channel.Reader.ReadAllAsync(ct)) await HandleAsync(o);
```

Bounded channels give **backpressure** (producers slow down when consumers fall behind), and several consumer tasks give parallelism with a fixed degree.

## 5. Parallelism Done Right

- Parallelize **CPU-bound** work; for I/O, use async concurrency instead.
- Partition work so threads don't contend on shared state; aggregate per-thread results at the end (`Parallel.For` with thread-local state).
- Measure: parallel overhead and contention can make code slower than sequential.

## Common Pitfalls

1. **`count++` from multiple threads**: a read-modify-write race; use `Interlocked`.
2. **Locking on `this`, types or strings**: other code can lock the same object.
3. **Blocking thread pool threads** with `.Result`, `Thread.Sleep` or sync I/O in servers.
4. **Inconsistent lock ordering**: deadlocks under load.
5. **`Parallel.ForEach` with async lambdas**: it doesn't await them; use `Parallel.ForEachAsync`.

## Quick Reference

```text
thread pool + Task · async for I/O · Parallel/PLINQ for CPU · Parallel.ForEachAsync for bounded async
lock (short, no await) · Interlocked (atomic) · SemaphoreSlim (limit/async lock) · concurrent collections
deadlocks: consistent lock order, small critical sections, no callbacks under locks
Channels: bounded = backpressure · BackgroundService for long-running loops · measure parallel speedups
```
