10. Shared state and the thread pool
Sections
Reading settings
Natural voice recorded for this course, the same in every browser.
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Concurrency bugs do not require unlucky hardware. Two workers can read the same value, both compute the same successor and both write it. The executable lesson forces that schedule with a barrier: two increments leave 1.
Protect the invariant, not the line
Section titled “Protect the invariant, not the line”System.Threading.Lock is the modern .NET lock primitive. Use it when several reads and writes form one invariant. Interlocked is better for one atomic counter or compare-and-swap loop. A concurrent collection makes its own operation safe; it does not make a sequence such as “check, call a remote service, then update” atomic.
The measured program runs 10,000 updates through both mechanisms:
two increments without synchronization: 1Lock count: 10000Interlocked count: 10000AddOrUpdate count: 1000ConcurrentDictionary.AddOrUpdate may invoke a value factory more than once under contention. Keep factories pure and treat side effects as a separate, idempotent operation.
Parallelism is a budget
Section titled “Parallelism is a budget”Parallel.ForEachAsync bounds concurrently active bodies with MaxDegreeOfParallelism. The lesson observes exactly two active bodies for a configured degree of two. This is a local budget, not end-to-end backpressure: the database pool, sockets and broker queues need their own limits.
Thread-pool starvation happens when worker threads block while queued work needs those workers to make progress. Prefer asynchronous waits for I/O, keep CPU work bounded, and diagnose with dotnet-counters before raising minimum threads. More threads can hide the symptom while increasing memory and context switching.
Run the proof
Section titled “Run the proof”dotnet run --project code/csharp-advanced/Advanced -c Release -- l10The portable assertions are stored in expected/l10.txt; machine-dependent thread-pool minimums are printed with # and excluded from comparison.
Exercises
Section titled “Exercises”- Replace the
Lockcounter with a two-field invariant: total and checksum. Show why twoInterlocked.Incrementcalls do not make the pair atomic. - Put an HTTP call inside
ConcurrentDictionary.GetOrAdd. Count how many calls occur under contention, then remove the side effect. - Compare
Parallel.ForEachAsyncdegrees 2, 8 and 64 against a dependency limited to four concurrent calls.
Solutions
- Protect both fields with one
Lock, or replace the pair atomically as one immutable value with compare-and-swap. Two independent atomic writes expose an intermediate state. - The factory may run repeatedly even though only one value wins. Cache a task only when its failure/cancellation semantics are intentional, or perform an idempotent operation outside the collection primitive.
- Degree 2 underuses the dependency, 8 adds queuing, and 64 usually adds latency without throughput. The falsifier is measured throughput and tail latency, not CPU count alone.