Two threads touch the same data. One of them must wait, or one of them must win cleanly. Every tool on this page is a different way to make that happen.
This page assumes you know async and await from A 05. Async is about waiting without a thread. Concurrency is about many threads running at once and sharing state. Each topic follows the same order: what it is, why it exists, a compiled sample, its pitfalls, and interview questions.
Thread is one OS thread. You start it, you join it, and it costs about 1 MB of stack.ThreadPool is a shared set of reused worker threads. You queue work items. You get no handle back.Task is a promise of a result. Task.Run puts work on the pool. A task carries the result, the exception, and supports await.Creating threads is slow and each one holds memory. The pool amortizes that cost. Tasks add composition on top: WhenAll, continuations, cancellation, and error flow. In modern code you almost always want a task.
Reading the figure. Blue is the code you write. Amber is the queue that holds pending work. Green boxes are pool threads that pick work off the queues. The violet box is a raw Thread. It skips the pool and keeps its own OS thread.
public static class ThreeWays
{
/// <summary>Runs the same tiny job on a Thread, the ThreadPool, and a Task.</summary>
/// <returns>The three results in a fixed order.</returns>
/// <example><c>ThreeWays.Demo()</c> returns "thread=1 pool=2 task=3".</example>
public static string Demo()
{
// 1. A raw Thread. You own its life, so Join waits for it to end.
int fromThread = 0; // 0 means "not run yet"
var thread = new Thread(() => fromThread = 1) { IsBackground = true };
thread.Start();
thread.Join(); // Join also makes the write visible here
// 2. The pool gives no handle back, so signal with an event.
int fromPool = 0;
using var done = new ManualResetEventSlim(false); // false: starts unsignaled
ThreadPool.QueueUserWorkItem(_ => { fromPool = 2; done.Set(); });
done.Wait();
// 3. A Task runs on the pool and carries the result back.
// .Result blocks. That is fine in a console demo, not in UI or ASP.NET code.
int fromTask = Task.Run(() => 3).Result;
// The values 1, 2, 3 only mark which path ran.
return $"thread={fromThread} pool={fromPool} task={fromTask}";
}
}
Thread.Sleep or .Result inside pool work starves the pool. The pool grows slowly, so latency spikes.Task.Factory.StartNew(..., TaskCreationOptions.LongRunning). It gets its own thread.Thread is foreground by default. It keeps the process alive after Main returns. Set IsBackground = true when that is not wanted.Thread?Task.Run.Task a thread?A race happens when the result depends on how threads interleave. The classic case is count++. It looks like one step, but it is three: read, add one, write back.
Two threads can both read 5, both add one, and both write 6. One increment is lost. The bug shows up only under load, which makes it hard to find later.
Reading the figure. Time runs down. Blue boxes are reads. Amber boxes are writes. Both threads read 5 before either writes. The red cell shows the lost update.
public static class RaceDemo
{
/// <summary>Increments a shared int with plain ++ from many workers.</summary>
/// <param name="workers">Number of parallel loop bodies.</param>
/// <param name="perWorker">Increments done by each body.</param>
/// <returns>The final count. It is often LESS than workers * perWorker.</returns>
/// <example><c>Unsafe(4, 100_000)</c> may return 263_118.</example>
public static int Unsafe(int workers, int perWorker)
{
int count = 0; // 0: nothing counted yet
Parallel.For(0, workers, _ =>
{
// i counts this worker's increments. Invariant: i increments tried so far.
for (int i = 0; i < perWorker; i++)
{
count++; // BUG: read, add, write can interleave
}
});
return count;
}
/// <summary>Same loop, but each increment is one atomic step.</summary>
/// <param name="workers">Number of parallel loop bodies.</param>
/// <param name="perWorker">Increments done by each body.</param>
/// <returns>Exactly workers * perWorker.</returns>
/// <example><c>Safe(4, 100_000)</c> returns 400_000.</example>
public static int Safe(int workers, int perWorker)
{
int count = 0;
Parallel.For(0, workers, _ =>
{
for (int i = 0; i < perWorker; i++)
{
Interlocked.Increment(ref count); // one CPU instruction, cannot be split
}
});
return count;
}
}
if (!dict.ContainsKey(k)) dict.Add(k, v) is a race even if each call is safe. The pair must be atomic.long or decimal write is not atomic on every platform. Use Interlocked.Read or a lock.lock (x) { ... } lets one thread at a time into the block. Before .NET 9, x was any object and the compiler called Monitor.Enter and Monitor.Exit. C# 13 with .NET 9 adds a real lock type, System.Threading.Lock. When x is a Lock, the compiler calls x.EnterScope() instead. It is a little faster and states the intent in the type.
A lock turns several steps into one indivisible step for every other thread that uses the same lock. It also makes your writes visible to the next thread that takes the lock.
public sealed class BankAccount
{
// C# 13 / .NET 9: a dedicated lock object. lock (_gate) compiles to EnterScope.
private readonly Lock _gate = new();
private decimal _balance;
/// <summary>Reads the balance under the lock, so a decimal is never torn.</summary>
/// <returns>The current balance.</returns>
/// <example><c>new BankAccount().Balance</c> returns 0.</example>
public decimal Balance
{
get { lock (_gate) { return _balance; } }
}
/// <summary>Adds money. The read-add-write runs as one step.</summary>
/// <param name="amount">Amount to add, must be positive.</param>
/// <example><c>Deposit(5m)</c> raises Balance by 5.</example>
public void Deposit(decimal amount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
lock (_gate) { _balance += amount; }
}
/// <summary>Withdraws only if the money is there. Check and act share one lock.</summary>
/// <param name="amount">Amount to take.</param>
/// <returns>true if the money was taken.</returns>
/// <example>Balance 10, <c>TryWithdraw(20m)</c> returns false.</example>
public bool TryWithdraw(decimal amount)
{
lock (_gate)
{
// The check and the change sit in ONE critical section.
if (_balance < amount) return false;
_balance -= amount;
return true;
}
}
}
public static class LockDemo
{
/// <summary>Puts 100 in, then races many withdrawals of 1.</summary>
/// <param name="attempts">Number of parallel withdraw attempts.</param>
/// <returns>How many withdrawals succeeded. Never more than 100.</returns>
/// <example><c>Overdraw(150)</c> returns 100.</example>
public static int Overdraw(int attempts)
{
var account = new BankAccount();
account.Deposit(100m); // 100: the cap on successful withdrawals
int ok = 0;
Parallel.For(0, attempts, _ =>
{
if (account.TryWithdraw(1m)) Interlocked.Increment(ref ok); // 1m: one unit
});
return ok;
}
/// <summary>Shows EnterScope, the method lock (Lock) uses under the hood.</summary>
/// <returns>"held free": held inside the scope, free after it.</returns>
/// <example><c>LockDemo.Scope()</c> returns "held free".</example>
public static string Scope()
{
var gate = new Lock();
string inside;
// EnterScope returns a ref struct. Dispose at the end of using releases the lock.
using (gate.EnterScope())
{
inside = gate.IsHeldByCurrentThread ? "held" : "free";
}
string after = gate.IsHeldByCurrentThread ? "held" : "free";
return $"{inside} {after}";
}
}
this, a Type, or a string. Outside code can lock the same object and deadlock you. Use a private field.Lock to object makes lock fall back to Monitor. The compiler warns (CS9216). Keep the field typed as Lock.await inside lock. It is a compile error. The thread that resumes may not be the thread that owns the lock. Use SemaphoreSlim.WaitAsync instead.lock re-entrant?Monitor and Lock let the owning thread enter again. It must exit the same number of times.lock compile to for an object?Monitor.Enter(obj, ref lockTaken) inside a try, and Monitor.Exit in the finally if lockTaken is true. So an exception never leaves the lock held.Monitor is the engine under lock (object). It adds two things lock cannot express. TryEnter gives up after a timeout. Wait and Pulse let a thread sleep inside a lock until another thread says the state changed.
Timeouts let you detect trouble instead of hanging. Wait and Pulse build classic condition variables, such as “wait until the queue is not empty”.
public sealed class Mailbox
{
private readonly object _gate = new(); // Wait/Pulse need an object, not Lock
private string? _message; // null means the box is empty
/// <summary>Puts a message in and wakes any waiting reader.</summary>
/// <param name="text">The message.</param>
/// <example><c>Put("hi")</c> then <c>Take()</c> returns "hi".</example>
public void Put(string text)
{
lock (_gate)
{
_message = text;
Monitor.PulseAll(_gate); // wake readers so they re-check
}
}
/// <summary>Blocks until a message is there, then takes it.</summary>
/// <returns>The message.</returns>
/// <example>After <c>Put("hi")</c>, <c>Take()</c> returns "hi".</example>
public string Take()
{
lock (_gate)
{
// while, not if: a wake-up does not promise the state you want.
while (_message is null)
{
Monitor.Wait(_gate); // releases the lock while asleep
}
string text = _message;
_message = null;
return text;
}
}
}
public static class MonitorDemo
{
/// <summary>Another thread tries a lock we hold, with a short timeout.</summary>
/// <returns>"busy free": fails while held, succeeds after release.</returns>
/// <example><c>MonitorDemo.TryEnter()</c> returns "busy free".</example>
public static string TryEnter()
{
var gate = new object();
bool whileHeld, afterRelease;
lock (gate)
{
bool got = false;
// 50 ms: long enough to show a timeout, short enough for a test.
var t = new Thread(() =>
{
got = Monitor.TryEnter(gate, TimeSpan.FromMilliseconds(50));
if (got) Monitor.Exit(gate);
});
t.Start();
t.Join(); // we still hold gate, so it must fail
whileHeld = got;
}
afterRelease = Monitor.TryEnter(gate, TimeSpan.FromMilliseconds(50));
if (afterRelease) Monitor.Exit(gate);
return $"{(whileHeld ? "got" : "busy")} {(afterRelease ? "free" : "busy")}";
}
/// <summary>A reader blocks on an empty Mailbox until a writer puts a message.</summary>
/// <returns>The message the reader saw.</returns>
/// <example><c>MonitorDemo.Handoff()</c> returns "ping".</example>
public static string Handoff()
{
var box = new Mailbox();
string seen = "";
var reader = new Thread(() => seen = box.Take());
reader.Start();
box.Put("ping");
reader.Join();
return seen;
}
}
if instead of while around Wait. Another thread may grab the item first. Always re-check the condition.Pulse instead of PulseAll can wake the wrong kind of waiter, such as a producer when a consumer was needed. PulseAll is the safe default.Wait or Pulse without holding the lock throws SynchronizationLockException.Monitor.Wait release the lock?Wait takes the lock back before it returns.Interlocked gives single atomic CPU operations: Increment, Decrement, Add, Exchange and CompareExchange. Compare-and-swap (CAS, a conditional atomic write) is the building block. CompareExchange(ref x, new, expected) writes new only if x still equals expected. It always returns the old value.
No lock means no blocking and no deadlock. For one shared number it is the fastest correct choice. For anything that is not a built-in operation, wrap it in a CAS loop.
Reading the figure. Blue steps are private work. The amber step is the one atomic write. Green is success. The red path is the retry, taken only when another thread changed x in between.
public static class Cas
{
/// <summary>Atomically raises target to value if value is larger.</summary>
/// <param name="target">The shared maximum.</param>
/// <param name="value">A candidate.</param>
/// <returns>The maximum after this call.</returns>
/// <example>target 3, <c>InterlockedMax(ref target, 9)</c> returns 9.</example>
public static int InterlockedMax(ref int target, int value)
{
int seen = Volatile.Read(ref target);
// Invariant at the top of each pass: seen is a value target held recently.
while (true)
{
if (value <= seen) return seen; // nothing to do, already big enough
int old = Interlocked.CompareExchange(ref target, value, seen);
if (old == seen) return value; // our write landed
seen = old; // lost the race, retry with fresh value
}
}
/// <summary>Finds the max of values from many threads at once.</summary>
/// <param name="values">Input numbers.</param>
/// <returns>The largest value.</returns>
/// <example><c>MaxDemo([4, 17, 2])</c> returns 17.</example>
public static int MaxDemo(int[] values)
{
int max = int.MinValue; // int.MinValue: smaller than any input
Parallel.ForEach(values, v => InterlockedMax(ref max, v));
return max;
}
/// <summary>Uses Exchange as a one-time switch. Only the first caller wins.</summary>
/// <param name="callers">Number of threads racing for the switch.</param>
/// <returns>How many callers won. Always 1.</returns>
/// <example><c>OneWinner(8)</c> returns 1.</example>
public static int OneWinner(int callers)
{
int flag = 0; // 0 = not taken, 1 = taken
int winners = 0;
Parallel.For(0, callers, _ =>
{
// Exchange returns the OLD value. Only one caller ever sees 0.
if (Interlocked.Exchange(ref flag, 1) == 0) Interlocked.Increment(ref winners);
});
return winners;
}
}
Interlocked wins. Under heavy contention, a CAS loop can spin and waste CPU. For several related fields a lock is simpler and often just as fast.The compiler, the JIT and the CPU may reorder or cache memory reads and writes. That is fine for one thread. Other threads can then see stale or out-of-order values. volatile on a field, or Volatile.Read and Volatile.Write, stop that for one field. A volatile read has acquire semantics: later reads cannot move above it. A volatile write has release semantics: earlier writes cannot move below it.
Without it, the JIT may read a bool stop flag once and keep it in a register. The loop then never sees the change and spins forever.
public sealed class StopFlagWorker
{
// volatile: every read goes to memory, so the loop sees the change.
private volatile bool _stop;
/// <summary>Starts a spinning thread, then stops it with the flag.</summary>
/// <returns>"stopped" once the worker has seen the flag and exited.</returns>
/// <example><c>new StopFlagWorker().Run()</c> returns "stopped".</example>
public string Run()
{
long spins = 0; // long: a busy loop can pass int.MaxValue
var worker = new Thread(() =>
{
// Without volatile the JIT could hoist this read out of the loop.
while (!_stop) spins++;
});
worker.Start();
Thread.Sleep(10); // 10 ms: let the worker spin a bit
_stop = true; // volatile write, release semantics
worker.Join();
return "stopped";
}
}
public sealed class Config(string name)
{
public string Name { get; } = name;
}
public static class SafePublish
{
private static Config? _instance;
private static readonly Lock Gate = new();
/// <summary>Double-checked lazy creation, done right.</summary>
/// <returns>The single shared Config.</returns>
/// <example><c>Get().Name</c> returns "main".</example>
public static Config Get()
{
// Fast path: acquire read, so a non-null reference has its fields ready.
var current = Volatile.Read(ref _instance);
if (current is not null) return current;
lock (Gate)
{
// Check again: another thread may have created it while we waited.
current = _instance;
if (current is null)
{
current = new Config("main");
Volatile.Write(ref _instance, current); // release: publish after init
}
return current;
}
}
}
volatile is not atomic. _count++ on a volatile field still loses updates. Use Interlocked.Interlocked or Thread.MemoryBarrier() when you need both directions.Lazy<T>, lock and CancellationToken already contain the right barriers. Reach for volatile last.Lazy<T>, prevents that. Most code should just use Lazy<T>.A semaphore holds a count of permits. Wait takes one and blocks if none are left. Release gives one back. SemaphoreSlim is the light in-process version, and it has WaitAsync.
It does two jobs. With a count of N it caps how many tasks run at once, such as 4 HTTP calls. With a count of 1 it is an async-friendly mutex, the fix for “no await inside lock”. With a count of 0 it is a signal another thread can fire.
public sealed class PeakCounter
{
private int _inFlight;
private int _peak;
/// <summary>Marks one job as started and updates the peak.</summary>
/// <example>First call raises Peak to 1.</example>
public void Enter() => Cas.InterlockedMax(ref _peak, Interlocked.Increment(ref _inFlight));
/// <summary>Marks one job as finished.</summary>
/// <example>After Enter then Exit, nothing is in flight.</example>
public void Exit() => Interlocked.Decrement(ref _inFlight);
/// <summary>Most jobs ever running at once.</summary>
/// <example>With a limit of 2, Peak is at most 2.</example>
public int Peak => Volatile.Read(ref _peak);
}
public static class Throttle
{
/// <summary>Runs many async jobs but lets only limit run at once.</summary>
/// <param name="jobs">Number of jobs.</param>
/// <param name="limit">Most jobs allowed in flight.</param>
/// <returns>The highest number seen in flight. Never above limit.</returns>
/// <example><c>MaxInFlight(10, 2)</c> returns 2.</example>
public static int MaxInFlight(int jobs, int limit) =>
MaxInFlightAsync(jobs, limit).GetAwaiter().GetResult();
private static async Task<int> MaxInFlightAsync(int jobs, int limit)
{
// limit, limit: start with limit permits, never more than limit.
using var gate = new SemaphoreSlim(limit, limit);
var counter = new PeakCounter();
var tasks = Enumerable.Range(0, jobs).Select(async _ =>
{
await gate.WaitAsync(); // take a permit or wait for one
try
{
counter.Enter();
await Task.Delay(5); // 5 ms of fake I/O
counter.Exit();
}
finally
{
gate.Release(); // always give the permit back
}
});
await Task.WhenAll(tasks);
return counter.Peak;
}
}
public sealed class AsyncCache
{
// Count 1, max 1: an async mutex. Safe to await while holding it.
private readonly SemaphoreSlim _mutex = new(1, 1);
private readonly Dictionary<string, int> _data = [];
public int Loads;
/// <summary>Gets a value, loading it at most once even under concurrency.</summary>
/// <param name="key">The key.</param>
/// <returns>The value, the key length in this demo.</returns>
/// <example><c>await GetAsync("abc")</c> returns 3.</example>
public async Task<int> GetAsync(string key)
{
await _mutex.WaitAsync();
try
{
if (_data.TryGetValue(key, out int hit)) return hit;
await Task.Delay(1); // 1 ms: stands in for a slow load
Loads++; // safe: only one holder at a time
return _data[key] = key.Length;
}
finally
{
_mutex.Release();
}
}
/// <summary>Asks for the same key from 20 tasks at once.</summary>
/// <returns>How many real loads happened. Always 1.</returns>
/// <example><c>new AsyncCache().Demo()</c> returns 1.</example>
public int Demo()
{
// 20 tasks: plenty of overlap for the mutex to matter.
var all = Enumerable.Range(0, 20).Select(_ => GetAsync("key"));
Task.WhenAll(all).GetAwaiter().GetResult();
return Loads;
}
}
SemaphoreFullException. Always release in finally, and only after a successful wait.lock, a holder that waits again on a count-1 semaphore deadlocks itself.SemaphoreSlim or Semaphore?SemaphoreSlim for anything inside one process. It is cheaper and has WaitAsync. Semaphore wraps an OS object and can be named and shared across processes.Many readers may hold the lock together. A writer gets it alone. An upgradeable read lets one thread read, then turn into a writer without letting anyone in between.
For data that is read far more than written, plain lock makes readers queue behind each other for no reason. This lock lets them run in parallel.
public sealed class RwCache : IDisposable
{
private readonly ReaderWriterLockSlim _rw = new();
private readonly Dictionary<string, int> _map = [];
public int Builds;
/// <summary>Gets a value, building it once if missing.</summary>
/// <param name="key">The key.</param>
/// <param name="build">Makes the value. Runs at most once per key.</param>
/// <returns>The cached or new value.</returns>
/// <example><c>GetOrAdd("ab", k => k.Length)</c> returns 2.</example>
public int GetOrAdd(string key, Func<string, int> build)
{
// Fast path: a shared read lock. Many readers run in parallel.
_rw.EnterReadLock();
try
{
if (_map.TryGetValue(key, out int hit)) return hit;
}
finally { _rw.ExitReadLock(); }
// Slow path: only one upgradeable reader at a time, so no lost builds.
_rw.EnterUpgradeableReadLock();
try
{
if (_map.TryGetValue(key, out int hit)) return hit; // someone beat us
_rw.EnterWriteLock();
try
{
Builds++;
return _map[key] = build(key);
}
finally { _rw.ExitWriteLock(); }
}
finally { _rw.ExitUpgradeableReadLock(); }
}
/// <summary>Releases the OS resources the lock may hold.</summary>
/// <example><c>using var c = new RwCache();</c></example>
public void Dispose() => _rw.Dispose();
/// <summary>Hammers 3 keys from many threads.</summary>
/// <returns>Number of builds. Always 3, one per key.</returns>
/// <example><c>RwCache.Demo()</c> returns 3.</example>
public static int Demo()
{
using var cache = new RwCache();
string[] keys = ["a", "bb", "ccc"];
// 300 lookups over 3 keys: each key is asked for 100 times.
Parallel.For(0, 300, i => cache.GetOrAdd(keys[i % keys.Length], k => k.Length));
return cache.Builds;
}
}
lock. It pays off only when reads are long or very frequent.await while holding it.A deadlock is a cycle of waiting. Thread 1 holds A and wants B. Thread 2 holds B and wants A. Neither can move. Four conditions must all hold: mutual exclusion, hold and wait, no preemption, and circular wait.
Break any one condition and deadlock is impossible. The easiest to break is circular wait. Give every lock a global order and always take locks in that order.
Reading the figure. Green arrows mean “holds”. Red arrows mean “waits for”. On the left the red and green arrows close a loop. On the right every thread asks for A first, so no thread can hold B while waiting for A.
public sealed class Account(int id, decimal balance)
{
public int Id { get; } = id; // the global lock order key
public decimal Balance { get; set; } = balance;
public Lock Gate { get; } = new();
}
public static class Transfers
{
/// <summary>Moves money between two accounts without deadlock.</summary>
/// <param name="from">Source account.</param>
/// <param name="to">Target account.</param>
/// <param name="amount">Amount to move.</param>
/// <example><c>Move(a, b, 5m)</c> takes 5 from a and gives it to b.</example>
public static void Move(Account from, Account to, decimal amount)
{
// Lock ordering: always lock the lower Id first, whatever the direction.
var (first, second) = from.Id < to.Id ? (from, to) : (to, from);
lock (first.Gate)
{
lock (second.Gate)
{
from.Balance -= amount;
to.Balance += amount;
}
}
}
/// <summary>Two threads move money in opposite directions many times.</summary>
/// <returns>The total money left. Always 200, and the call always returns.</returns>
/// <example><c>Transfers.Demo()</c> returns 200.</example>
public static decimal Demo()
{
var a = new Account(1, 100m); // ids 1 and 2 fix the order
var b = new Account(2, 100m); // 100 + 100 = 200 total
// 10_000 rounds: enough overlap that a naive version would hang.
var t1 = new Thread(() => { for (int i = 0; i < 10_000; i++) Move(a, b, 1m); });
var t2 = new Thread(() => { for (int i = 0; i < 10_000; i++) Move(b, a, 1m); });
t1.Start(); t2.Start();
t1.Join(); t2.Join();
return a.Balance + b.Balance;
}
}
public static class DeadlockDetector
{
/// <summary>Forces the A-then-B and B-then-A cycle, and detects it with timeouts.</summary>
/// <returns>"deadlock": both threads timed out waiting for the other lock.</returns>
/// <example><c>DeadlockDetector.Opposite()</c> returns "deadlock".</example>
public static string Opposite()
{
var a = new object();
var b = new object();
// 2 parties: the two threads. Barrier 1 makes both hold their first lock.
// Barrier 2 keeps both locks held until both tries have failed.
using var bothHoldFirst = new Barrier(2);
using var bothTried = new Barrier(2);
bool got1 = false, got2 = false;
void Run(object mine, object theirs, Action<bool> report)
{
lock (mine)
{
bothHoldFirst.SignalAndWait();
// 100 ms: plenty of time if the lock were free. It never is.
bool got = Monitor.TryEnter(theirs, TimeSpan.FromMilliseconds(100));
if (got) Monitor.Exit(theirs);
report(got);
bothTried.SignalAndWait();
}
}
var t1 = new Thread(() => Run(a, b, g => got1 = g));
var t2 = new Thread(() => Run(b, a, g => got2 = g));
t1.Start(); t2.Start();
t1.Join(); t2.Join();
return !got1 && !got2 ? "deadlock" : "no deadlock";
}
}
.Result can deadlock with no lock at all. The task needs the blocked UI or legacy ASP.NET context to resume.SemaphoreSlim(1, 1) waited twice by the same flow.dotnet-dump collect. Then run clrstack -all and syncblk to see which thread holds which lock and what it waits for.TryEnter and back off. Or avoid shared state with messages, immutable data, or a single owner thread.System.Collections.Concurrent holds thread-safe collections. ConcurrentDictionary uses fine-grained locks and lock-free reads. ConcurrentQueue, ConcurrentStack and ConcurrentBag are lock-free. BlockingCollection wraps one of them and adds blocking and a bounded size.
Each single call is atomic, so you skip writing locks by hand. But a sequence of calls is still not atomic. The API gives combined calls such as GetOrAdd, AddOrUpdate and TryRemove for that reason.
GetOrAdd(key, factory) stores only one value per key. But the factory runs outside the lock. Under contention it can run several times, and the extra results are thrown away. If the factory is costly or has side effects, store a Lazy<T> instead.
using System.Collections.Concurrent;
public static class GetOrAddDemo
{
/// <summary>Many threads ask for one missing key at the same moment.</summary>
/// <param name="threads">How many threads race.</param>
/// <returns>How many times the factory ran. Can be more than 1.</returns>
/// <example><c>NaiveFactoryRuns(8)</c> may return 8.</example>
public static int NaiveFactoryRuns(int threads)
{
var map = new ConcurrentDictionary<string, int>();
int runs = 0;
RaceAll(threads, () => map.GetOrAdd("k", _ =>
{
Interlocked.Increment(ref runs);
Thread.Sleep(20); // 20 ms: a slow factory widens the race
return 42; // 42: any value, all copies are equal
}));
return runs;
}
/// <summary>Same race, but the dictionary stores Lazy values.</summary>
/// <param name="threads">How many threads race.</param>
/// <returns>How many times the real factory ran. Always 1.</returns>
/// <example><c>LazyFactoryRuns(8)</c> returns 1.</example>
public static int LazyFactoryRuns(int threads)
{
var map = new ConcurrentDictionary<string, Lazy<int>>();
int runs = 0;
RaceAll(threads, () =>
{
// Many Lazy shells may be built, but only the stored one is ever .Value-d.
var lazy = map.GetOrAdd("k", _ => new Lazy<int>(() =>
{
Interlocked.Increment(ref runs);
Thread.Sleep(20);
return 42;
}));
_ = lazy.Value;
});
return runs;
}
// Starts n raw threads that all run body right after a shared barrier.
// Raw threads, not Parallel.For: a barrier needs every party running at once.
private static void RaceAll(int n, Action body)
{
using var start = new Barrier(n);
var all = Enumerable.Range(0, n)
.Select(_ => new Thread(() => { start.SignalAndWait(); body(); }))
.ToList();
all.ForEach(t => t.Start());
all.ForEach(t => t.Join());
}
}
public static class WordCounter
{
/// <summary>Counts words from many threads with AddOrUpdate.</summary>
/// <param name="lines">Input lines.</param>
/// <returns>"word=count" pairs sorted by word.</returns>
/// <example><c>Count(["a b", "a"])</c> returns ["a=2", "b=1"].</example>
public static List<string> Count(string[] lines)
{
var counts = new ConcurrentDictionary<string, int>();
Parallel.ForEach(lines, line =>
{
foreach (var word in line.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
// 1 if new. Otherwise old + 1. The update lambda may retry, so keep it pure.
counts.AddOrUpdate(word, 1, (_, old) => old + 1);
}
});
// Dictionary order is not defined, so sort for a stable answer.
return [.. counts.OrderBy(p => p.Key, StringComparer.Ordinal)
.Select(p => $"{p.Key}={p.Value}")];
}
}
public static class QueueDemo
{
/// <summary>4 producers enqueue 1..n each. One drain sums everything.</summary>
/// <param name="n">Items per producer.</param>
/// <returns>The total. Equals 4 * n * (n + 1) / 2.</returns>
/// <example><c>Sum(10)</c> returns 220.</example>
public static long Sum(int n)
{
var queue = new ConcurrentQueue<int>();
// 4 producers: enough to show contention. 1..n each: values start at 1.
Parallel.For(0, 4, _ =>
{
for (int v = 1; v <= n; v++) queue.Enqueue(v);
});
long total = 0;
// TryDequeue never blocks. It returns false when the queue is empty.
while (queue.TryDequeue(out int v)) total += v;
return total;
}
}
public static class Pipeline
{
/// <summary>Producer and consumer joined by a bounded BlockingCollection.</summary>
/// <param name="n">Number of items to send, 1..n.</param>
/// <returns>Sum of the squares the consumer saw, in order.</returns>
/// <example><c>SumOfSquares(3)</c> returns 14.</example>
public static long SumOfSquares(int n)
{
// boundedCapacity 2: the producer blocks when 2 items wait. That is back-pressure.
using var buffer = new BlockingCollection<int>(boundedCapacity: 2);
var producer = Task.Run(() =>
{
for (int i = 1; i <= n; i++) buffer.Add(i); // blocks while full
buffer.CompleteAdding(); // tells the consumer no more items
});
long total = 0;
// GetConsumingEnumerable blocks while empty and ends after CompleteAdding.
foreach (int item in buffer.GetConsumingEnumerable()) total += (long)item * item;
producer.Wait();
return total;
}
}
Lazy<T>.Count, Keys and ToArray take every internal lock. Calling them in a hot loop kills throughput.BlockingCollection blocks threads. In async code use System.Threading.Channels instead, the async version of the same idea.ConcurrentBag is tuned for threads that add and take their own items. As a general queue it is slow.dict.GetOrAdd(k, f) guaranteed to call f once?f can run on several threads. Use Lazy<T> values for once-only creation.Dictionary in a lock?ConcurrentDictionary wins when many threads read and write at once, because reads take no lock.System.Collections.Immutable gives lists, dictionaries and sets that never change. Add returns a new collection that shares most of its structure with the old one. System.Collections.Frozen (.NET 8) gives read-only sets and dictionaries tuned for fast lookups after a one-time build.
Data that never changes needs no lock to read. A reader holds a snapshot that stays valid forever. Writers build a new version and swap one reference.
using System.Collections.Frozen;
using System.Collections.Immutable;
public static class ImmutableDemo
{
/// <summary>Add returns a new list. The old one is untouched.</summary>
/// <returns>"3 4": old count, new count.</returns>
/// <example><c>ImmutableDemo.Snapshot()</c> returns "3 4".</example>
public static string Snapshot()
{
ImmutableList<int> before = [1, 2, 3];
var after = before.Add(4); // 4: one new item
return $"{before.Count} {after.Count}";
}
/// <summary>Many threads add to one shared immutable list without a lock.</summary>
/// <param name="n">Number of adds.</param>
/// <returns>Final count. Always n.</returns>
/// <example><c>ConcurrentAdds(1000)</c> returns 1000.</example>
public static int ConcurrentAdds(int n)
{
var list = ImmutableList<int>.Empty;
// ImmutableInterlocked.Update is a CAS loop on the reference.
Parallel.For(0, n, i => ImmutableInterlocked.Update(ref list, l => l.Add(i)));
return list.Count;
}
/// <summary>Builds a frozen lookup once, then reads it from anywhere.</summary>
/// <param name="key">A key to look up.</param>
/// <returns>The code, or -1 if the key is unknown.</returns>
/// <example><c>Lookup("GBP")</c> returns 826.</example>
public static int Lookup(string key)
{
// ISO 4217 numeric codes, used here as sample values.
var raw = new Dictionary<string, int> { ["USD"] = 840, ["EUR"] = 978, ["GBP"] = 826 };
var codes = raw.ToFrozenDictionary();
return codes.TryGetValue(key, out int code) ? code : -1; // -1: not found
}
}
ImmutableList is a balanced tree, so indexing is O(log n). Use a Builder for many changes in a row.ReadOnlyCollection is not immutable. It is a view. The owner can still change the list under you.Interlocked.CompareExchange. Retry if it changed. ImmutableInterlocked.Update does exactly that.Lazy<T> runs a factory the first time someone reads .Value, then caches the result. The default mode, ExecutionAndPublication, runs the factory exactly once even when many threads ask at once.
It is the correct, ready-made form of double-checked locking. It defers costly work until needed.
public static class LazyDemo
{
/// <summary>Many threads read one Lazy at the same moment.</summary>
/// <param name="threads">Number of readers.</param>
/// <returns>Factory runs. Always 1 in the default mode.</returns>
/// <example><c>FactoryRuns(8)</c> returns 1.</example>
public static int FactoryRuns(int threads)
{
int runs = 0;
var lazy = new Lazy<int>(() =>
{
Interlocked.Increment(ref runs);
Thread.Sleep(20); // slow factory, to invite a race
return 7; // 7: any value
}, LazyThreadSafetyMode.ExecutionAndPublication);
Parallel.For(0, threads, _ => _ = lazy.Value);
return runs;
}
/// <summary>Shows that the default mode caches an exception.</summary>
/// <returns>"1 1": the factory ran once and both reads threw.</returns>
/// <example><c>LazyDemo.CachedFailure()</c> returns "1 1".</example>
public static string CachedFailure()
{
int runs = 0, throws = 0;
var lazy = new Lazy<int>(() =>
{
runs++;
throw new InvalidOperationException("boom");
});
// 2 reads: proves the second read does not retry the factory.
for (int i = 0; i < 2; i++)
{
try { _ = lazy.Value; } catch (InvalidOperationException) { throws++; }
}
// throws is 2, runs is 1. Report runs and throws / 2 to keep it short.
return $"{runs} {throws / 2}";
}
}
PublicationOnly to allow retries. That mode may run the factory several times and keep the first result..Value throws InvalidOperationException.Lazy<Task<T>> works but caches a faulted task too.private static readonly Lazy<Foo> Inst = new(() => new Foo()); and expose Inst.Value. A static readonly field also works, because the runtime runs a static constructor once.ThreadLocal<T> gives each thread its own copy. [ThreadStatic] is the older field form.AsyncLocal<T> gives each logical flow its own copy. The value follows await to whatever thread resumes.ThreadLocal avoids sharing, so it suits per-thread caches and scratch buffers. AsyncLocal carries ambient context such as a request id or a trace span through async calls.
public static class LocalsDemo
{
private static readonly AsyncLocal<string?> RequestId = new();
/// <summary>Shows that AsyncLocal flows down into awaits but not back up.</summary>
/// <returns>"req-1 req-1": the value survives an await and a child's change.</returns>
/// <example><c>LocalsDemo.Flow()</c> returns "req-1 req-1".</example>
public static string Flow() => FlowAsync().GetAwaiter().GetResult();
private static async Task<string> FlowAsync()
{
RequestId.Value = "req-1";
await Task.Yield(); // may resume on another thread
string afterAwait = RequestId.Value ?? "none";
await ChildAsync(); // child sets its own value
// The child's change ended with the child. We still see ours.
return $"{afterAwait} {RequestId.Value ?? "none"}";
}
private static async Task ChildAsync()
{
RequestId.Value = "child";
await Task.Yield();
}
/// <summary>Each thread counts in its own slot. Slots are summed at the end.</summary>
/// <param name="n">Number of loop iterations.</param>
/// <returns>The total count. Always n.</returns>
/// <example><c>ThreadLocalSum(1000)</c> returns 1000.</example>
public static int ThreadLocalSum(int n)
{
// () => 0: each thread starts its slot at 0. trackAllValues lets us read all slots.
using var slot = new ThreadLocal<int>(() => 0, trackAllValues: true);
Parallel.For(0, n, _ => slot.Value++); // no sharing, so no lock
return slot.Values.Sum();
}
}
await. After an await you may be on a different thread and see a different value. Use AsyncLocal for async flows.[ThreadStatic] initializers run once, on the first thread only. Other threads see the default. Initialize lazily or use ThreadLocal.AsyncLocal flow?ExecutionContext. await, Task.Run and thread pool queues capture and restore that context. Changes are copy-on-write, so a child cannot change the parent's value.Parallel.For and Parallel.ForEach split a loop across pool threads. Parallel.ForEachAsync (.NET 6) does the same for async bodies with a parallelism cap. PLINQ, started with .AsParallel(), runs a LINQ query in parallel.
They are for CPU-bound data work: many independent items and enough work per item to beat the scheduling cost.
public static class ParallelDemo
{
/// <summary>Sum of i*i for i in 1..n, with thread-local subtotals.</summary>
/// <param name="n">Upper bound, inclusive.</param>
/// <returns>The sum.</returns>
/// <example><c>SumOfSquares(100)</c> returns 338350.</example>
public static long SumOfSquares(int n)
{
long total = 0;
// 1, n + 1: For is [from, to), so + 1 makes n included.
Parallel.For(1, n + 1,
() => 0L, // each worker starts at 0
(i, _, local) => local + (long)i * i, // no sharing in the hot path
local => Interlocked.Add(ref total, local)); // one atomic add per worker
return total;
}
/// <summary>Primes up to limit, with PLINQ, kept in input order.</summary>
/// <param name="limit">Largest number to test.</param>
/// <returns>The primes, ascending.</returns>
/// <example><c>Primes(20)</c> returns [2, 3, 5, 7, 11, 13, 17, 19].</example>
public static int[] Primes(int limit) =>
// Start at 2, the first prime. limit - 1 values cover 2..limit.
Enumerable.Range(2, limit - 1)
.AsParallel()
.AsOrdered() // without this, order is not defined
.Where(IsPrime)
.ToArray();
private static bool IsPrime(int n)
{
// d * d <= n: a factor above sqrt(n) pairs with one below it.
for (int d = 2; d * d <= n; d++)
{
if (n % d == 0) return false; // % d == 0: d divides n
}
return true;
}
/// <summary>Async bodies with a cap on how many run at once.</summary>
/// <param name="items">Inputs.</param>
/// <returns>Sum of the doubled inputs.</returns>
/// <example><c>DoubleAll([1, 2, 3])</c> returns 12.</example>
public static int DoubleAll(int[] items)
{
int sum = 0;
// 2: at most two bodies in flight, like a small HTTP connection limit.
var options = new ParallelOptions { MaxDegreeOfParallelism = 2 };
Parallel.ForEachAsync(items, options, async (x, ct) =>
{
await Task.Delay(1, ct); // 1 ms of fake I/O
Interlocked.Add(ref sum, x * 2); // * 2: the "work" is doubling
}).GetAwaiter().GetResult();
return sum;
}
/// <summary>Exceptions from parallel bodies arrive wrapped together.</summary>
/// <returns>The exception type name.</returns>
/// <example><c>ParallelDemo.Failure()</c> returns "AggregateException".</example>
public static string Failure()
{
try
{
// 0..3: four bodies, and body 2 fails.
Parallel.For(0, 4, i => { if (i == 2) throw new InvalidOperationException(); });
return "none";
}
catch (AggregateException ex)
{
return ex.GetType().Name;
}
}
}
Parallel.ForEach with an async lambda becomes async void. The loop returns before the work ends. Use Parallel.ForEachAsync.SumOfSquares.AsOrdered.Parallel.ForEach or Task.WhenAll?Parallel for CPU work on many items. Task.WhenAll for I/O work that waits without threads. For async work that needs a cap, use Parallel.ForEachAsync.Build a thread-safe FIFO queue with a fixed capacity. Enqueue blocks while the queue is full. Dequeue blocks while it is empty. Count returns the current size. This is LeetCode 1188.
Use two semaphores. _free counts empty slots and starts at the capacity. _items counts filled slots and starts at 0. A producer takes a free slot, adds, then releases an item. A consumer does the mirror. A small lock guards the ring buffer itself.
Reading the figure. Green cells hold items. Grey cells are empty. The two amber counters always add up to the capacity, 4. A producer blocks when _free is 0. A consumer blocks when _items is 0.
public sealed class BoundedBlockingQueue<T> : IDisposable
{
private readonly T[] _ring;
private int _head; // index of the oldest item
private int _count; // items stored now
private readonly Lock _gate = new(); // guards _ring, _head, _count
private readonly SemaphoreSlim _free; // empty slots
private readonly SemaphoreSlim _items; // filled slots
/// <summary>Creates a queue that holds at most capacity items.</summary>
/// <param name="capacity">Maximum size, at least 1.</param>
/// <example><c>new BoundedBlockingQueue<int>(2)</c></example>
public BoundedBlockingQueue(int capacity)
{
ArgumentOutOfRangeException.ThrowIfLessThan(capacity, 1); // 1: smallest useful
_ring = new T[capacity];
_free = new SemaphoreSlim(capacity, capacity); // all slots start empty
_items = new SemaphoreSlim(0, capacity); // 0: nothing to take yet
}
/// <summary>Adds an item, blocking while the queue is full.</summary>
/// <param name="item">The item.</param>
/// <example><c>q.Enqueue(5)</c></example>
public void Enqueue(T item)
{
_free.Wait(); // 1. reserve an empty slot
lock (_gate)
{
// Tail = head + count, wrapped by % length to stay inside the ring.
_ring[(_head + _count) % _ring.Length] = item;
_count++;
}
_items.Release(); // 2. announce a filled slot
}
/// <summary>Tries to add within a timeout.</summary>
/// <param name="item">The item.</param>
/// <param name="timeoutMs">Milliseconds to wait for room.</param>
/// <returns>false if the queue stayed full the whole time.</returns>
/// <example>On a full queue, <c>TryEnqueue(1, 10)</c> returns false.</example>
public bool TryEnqueue(T item, int timeoutMs)
{
if (!_free.Wait(timeoutMs)) return false; // no slot freed in time
lock (_gate)
{
_ring[(_head + _count) % _ring.Length] = item;
_count++;
}
_items.Release();
return true;
}
/// <summary>Removes the oldest item, blocking while the queue is empty.</summary>
/// <returns>The oldest item.</returns>
/// <example>After <c>Enqueue(5)</c>, <c>Dequeue()</c> returns 5.</example>
public T Dequeue()
{
_items.Wait(); // 1. reserve a filled slot
T item;
lock (_gate)
{
item = _ring[_head];
_ring[_head] = default!; // drop the reference for the GC
_head = (_head + 1) % _ring.Length; // + 1 then wrap: next oldest
_count--;
}
_free.Release(); // 2. announce an empty slot
return item;
}
/// <summary>Current number of items.</summary>
/// <example>New queue: <c>Count</c> is 0.</example>
public int Count { get { lock (_gate) { return _count; } } }
/// <summary>Frees the two semaphores.</summary>
/// <example><c>using var q = new BoundedBlockingQueue<int>(4);</c></example>
public void Dispose() { _free.Dispose(); _items.Dispose(); }
}
public static class BbqDemo
{
/// <summary>3 producers and 3 consumers share a queue of capacity 2.</summary>
/// <param name="perProducer">Items each producer sends, values 1..perProducer.</param>
/// <returns>The sum consumed. Equals 3 * p * (p + 1) / 2.</returns>
/// <example><c>Sum(100)</c> returns 15150.</example>
public static long Sum(int perProducer)
{
// 2: a tiny capacity forces lots of blocking on both sides.
using var q = new BoundedBlockingQueue<int>(2);
const int Workers = 3; // 3 producers and 3 consumers
long total = 0;
var producers = Enumerable.Range(0, Workers).Select(_ => new Thread(() =>
{
for (int v = 1; v <= perProducer; v++) q.Enqueue(v);
})).ToList();
// Each consumer takes an equal share, so all of them finish.
var consumers = Enumerable.Range(0, Workers).Select(_ => new Thread(() =>
{
for (int k = 0; k < perProducer; k++) Interlocked.Add(ref total, q.Dequeue());
})).ToList();
producers.Concat(consumers).ToList().ForEach(t => t.Start());
producers.Concat(consumers).ToList().ForEach(t => t.Join());
return total;
}
/// <summary>One producer, one consumer: FIFO order must hold.</summary>
/// <returns>The items in the order they came out.</returns>
/// <example><c>BbqDemo.Order()</c> returns [1, 2, 3, 4, 5].</example>
public static List<int> Order()
{
using var q = new BoundedBlockingQueue<int>(2);
var seen = new List<int>();
// 5 items through capacity 2: the producer must block at least once.
var consumer = new Thread(() =>
{
for (int k = 0; k < 5; k++) seen.Add(q.Dequeue());
});
consumer.Start();
for (int v = 1; v <= 5; v++) q.Enqueue(v);
consumer.Join();
return seen;
}
/// <summary>A full queue refuses TryEnqueue after the timeout.</summary>
/// <returns>"True False 1": first add works, second times out, size is 1.</returns>
/// <example><c>BbqDemo.Full()</c> returns "True False 1".</example>
public static string Full()
{
using var q = new BoundedBlockingQueue<string>(1); // 1: full after one add
bool first = q.TryEnqueue("a", 10); // 10 ms timeout, plenty for a free slot
bool second = q.TryEnqueue("b", 10); // no consumer, so this must time out
return $"{first} {second} {q.Count}";
}
}
The classic alternative uses one lock with Monitor.Wait and Monitor.PulseAll. The producer waits while count == capacity. The consumer waits while count == 0. Both pulse after each change. Know both. The semaphore version is easier to get right, and it ports to async with WaitAsync.
CancellationToken to Wait so blocked threads can leave.Three threads call First, Second and Third on one object, in any order. The output must always be “firstsecondthird”. This is LeetCode 1114.
Two semaphores that start at 0 act as one-shot gates. First prints and opens gate 1. Second waits on gate 1, prints and opens gate 2. Third waits on gate 2.
public sealed class PrintInOrder : IDisposable
{
// 0, 1: start closed, open at most once. A Release is the "go" signal.
private readonly SemaphoreSlim _firstDone = new(0, 1);
private readonly SemaphoreSlim _secondDone = new(0, 1);
/// <summary>Runs printFirst with no waiting, then opens gate 1.</summary>
/// <param name="printFirst">Prints "first".</param>
/// <example>Called in any order, output is "firstsecondthird".</example>
public void First(Action printFirst)
{
printFirst();
_firstDone.Release();
}
/// <summary>Waits for gate 1, runs printSecond, opens gate 2.</summary>
/// <param name="printSecond">Prints "second".</param>
/// <example>Never prints before First has printed.</example>
public void Second(Action printSecond)
{
_firstDone.Wait();
printSecond();
_secondDone.Release();
}
/// <summary>Waits for gate 2, then runs printThird.</summary>
/// <param name="printThird">Prints "third".</param>
/// <example>Always prints last.</example>
public void Third(Action printThird)
{
_secondDone.Wait();
printThird();
}
/// <summary>Frees the gates.</summary>
/// <example><c>using var p = new PrintInOrder();</c></example>
public void Dispose() { _firstDone.Dispose(); _secondDone.Dispose(); }
/// <summary>Starts the three calls in a given thread start order.</summary>
/// <param name="startOrder">Which call each thread makes, e.g. [3, 2, 1].</param>
/// <returns>The printed text. Always "firstsecondthird".</returns>
/// <example><c>Run([3, 1, 2])</c> returns "firstsecondthird".</example>
public static string Run(int[] startOrder)
{
using var p = new PrintInOrder();
var output = new System.Text.StringBuilder();
var outGate = new Lock(); // StringBuilder is not thread-safe
void Print(string s) { lock (outGate) { output.Append(s); } }
var threads = startOrder.Select(n => new Thread(() =>
{
// 1, 2, 3 name the three methods of the problem.
switch (n)
{
case 1: p.First(() => Print("first")); break;
case 2: p.Second(() => Print("second")); break;
default: p.Third(() => Print("third")); break;
}
})).ToList();
threads.ForEach(t => t.Start());
threads.ForEach(t => t.Join());
return output.ToString();
}
}
Wait for await WaitAsync, or use TaskCompletionSource as a one-shot gate.ManualResetEventSlim? It works too for one-shot gates. Semaphores also work for repeated rounds..Result or Sleep.count++ is three steps. Use Interlocked for one number and a lock for several.System.Threading.Lock field. Never lock this or a string.await inside lock. Use SemaphoreSlim(1, 1) with WaitAsync.volatile fixes visibility, not atomicity. Most code should use Lazy<T> or a lock instead.GetOrAdd factories can run more than once. Store Lazy<T> for once-only work.await.The last page is a tour of the language itself. A 11 walks every C# version from 7.0 to 14 and links each feature back to the page that covers it.