Concurrency: sharing memory without lying
The scheduler interleaves your threads wherever it pleases; correctness under that is a discipline.
The scheduler lie has a sharp edge, sharp enough to deserve its own stage: threads share the heap, and the scheduler interleaves them wherever it pleases. Correct sequential code, run concurrently, becomes a lottery — and the discipline that removes the lottery is small, learnable, and non-negotiable.
The lost update
Two threads that each do read, add one, write on the same counter can interleave so that one increment vanishes:
Both threads did everything right, locally; the result is still wrong, and only sometimes, which is what makes data races so expensive to find. The tools, in rough order of reach: a mutex makes a critical section exclusive (cheap uncontended — it is contention that costs, because contention means waiting); a read-write lock lets readers share; atomics (compare-and-swap) handle single-word updates without blocking; and message passing sidesteps sharing altogether by giving each piece of data one owner.
Locks bring their own failure mode. Deadlock needs four conditions at once — mutual exclusion, hold-and-wait, no preemption, and a cycle of waiting — and the practical defence is boringly effective: define one global order in which locks may be taken, and never hold a lock while doing I/O or waiting on anything slow.
The async alternative
Instead of many threads blocking on I/O, one thread can ask the kernel “tell me when any of these ten thousand sockets is ready” (epoll and friends) and hop between whichever are. That is the entire secret of the event loop; async/await is syntax for pausing a task at a marked point and resuming it later on the same thread. Interleaving still happens — but only at awaits, which is why single-threaded async code needs no locks around plain state, and why blocking the loop (a long computation, a synchronous file read) freezes every task at once. Async wins when you wait a lot on many things; threads-per-core win when you compute.
Where it bites
- Holding a lock across an
awaitor a network call — you have serialised the system on your slowest dependency. - CPU-heavy work on the event loop — one busy handler and every connection stalls.
- Unbounded thread or connection creation under load — the fix is a pool with a queue, which is System Design’s failure stage in miniature.
- Sprinkling locks until the race “goes away” — without knowing which invariant each lock guards, you have traded a race for a deadlock on layaway.