Concurrency & Parallelism
How do multiple pieces of work make progress safely, how do we coordinate shared state, how do we use multiple cores effectively, and why do concurrent systems fail in ways that testing does not catch?
What work are you trying to run?
The execution model is derived from the work, not chosen from a catalogue. Pick the work and the first question is about the work — never about a primitive.
Is every core already busy, or is one core busy and seven idle — and do you know which?
- 1Classifying the Work: Computing or Waiting?Before you can choose an execution model you have to know what the work is doing with the wall clock. Compression, resizing, encryption, parsing and numerics compute. Network calls, database queries, disk reads and third-party APIs wait. Most real handlers do both, in phases, and the phases want different models.
- 2Why Parallelism Exists: ComputeOne billion floats to sum. One core does it in 400 ms. Four cores should do it in 100 ms and will actually do it in about 130. Parallelism exists because a single core stopped getting faster around 2005 and the only remaining lever was more of them.
- 3Choosing an Execution ModelSeven questions — bound, sharing, independence, ordering, task count, duration, isolation — and seven answers: sequential, threads, processes, async, worker pool, message passing, parallel algorithm. Every answer comes with why it wins, what it costs and how it fails, because a recommendation without those three is a preference.
- 4Fork/JoinSplit the work, run the pieces, wait for all of them, combine. The split is easy and the combine is arithmetic — the join is the part that carries the correctness, because it is the only place a happens-before edge exists between the children and the parent that reads their results.
- 5Parallel OverheadSplitting, scheduling, synchronizing and combining are work the sequential version never does. For small tasks that overhead exceeds the task, and the parallel version is measurably slower on more hardware — which is why every parallel decomposition needs a size below which it stops decomposing.
- 6Amdahl's LawThe part that cannot be parallelised sets a ceiling on everything else. If a tenth of the job must happen in sequence, an infinite number of cores still cannot make it more than ten times faster — and long before infinity, each extra core is buying almost nothing.
The learning loop
Every lesson answers this chain as explicit fields — including the two that are never optional here: what invariant must hold under every interleaving, and what complexity the concurrency cost you.
Flagship experiences
Step a schedule one indivisible operation at a time and watch an invariant hold, then die. The trace is the evidence; the claim underneath it is what you take away.
You drive the scheduler. Interleave two actors' operations yourself and try to break the stated invariant — then see what each fix costs.
Acquire locks in whatever order you like, watch the wait-for graph grow an edge at a time, and find the cycle. Then break one of the four conditions and see which ones are actually available to you.
Move the serial fraction and the per-task overhead and watch the speedup curve bend away from linear. Amdahl and Gustafson answering different questions about the same program.
Every concurrent design is bought with complexity. Price one: what is shared, what must be synchronized, what can deadlock, and what the speedup was actually worth.
Convoys, oversubscription, priority inversion, a starved writer, a stalled event loop. Predict which one the symptom describes before the schedule is revealed.
Take a dependency graph, find the independent work, and read the ceiling off work versus span — including the cases where the honest answer is that it does not parallelise.
One system, end to end: classify the work, choose the execution model, name the invariants, defend the synchronization, then survive the schedules injected against your own design.
Learning modules
Should this work overlap, run simultaneously, or neither?
The distinction the whole domain rests on: overlapping progress versus simultaneous execution, why each exists, how to classify work as waiting-bound or compute-bound, and the honest admission that concurrency is always bought with complexity.
What unit actually carries this work, and who schedules it?
Processes, threads, tasks and coroutines as distinct things a runtime can hand you — and the language-specific reality of what each one buys in C++, JavaScript and CPython. The kernel-side mechanics live in Operating Systems.
What does suspending a task actually do, and what still runs?
Async/await as an execution model rather than syntax: futures and promises, the event loop, worker threads and web workers, and the two lessons that matter most — async is not parallelism, and a CPU-bound handler stalls the loop for everyone.
What is shared, what must stay true, and which schedule breaks it?
The core of the domain. Shared mutable state, invariants, finding the minimal critical section, enumerating interleavings to locate a failing schedule, and the distinction between a race condition and a data race that most engineers conflate.
Which guarantee does this primitive give, and which does it not?
Mutexes and the scope you put them around, read/write locks, semaphores as permit counters, condition variables and the predicate loop — with lost and spurious wakeups as the failures that follow from getting the loop wrong.
Can we avoid sharing state by moving data instead?
Producer/consumer as the flagship pattern, bounded versus unbounded queues, backpressure as the conversation between a fast producer and a slow consumer, channels, message passing and the actor model — coordination without shared memory.
How does a correct-looking system stop making progress?
The four conditions for deadlock and how to break each one, lock ordering as the practical answer, wait-for graphs and their cycles, livelock where everyone is busy and nothing advances, starvation, fairness and priority inversion.
Why did adding threads make it slower?
Eight cores all waiting on one lock. Convoys, false parallelism, oversubscription, the real cost of a context switch, and busy waiting — the module that explains why parallel hardware does not imply parallel throughput.
What does an atomic operation actually make indivisible?
Atomic reads, writes and compare-and-swap, why an atomic variable does not make a multi-step invariant safe, and lock-free concepts treated honestly as progress guarantees rather than performance promises — CAS loops, the ABA problem, wait-free versus lock-free.
Will the other thread even see what I wrote?
The advanced core: threads do not necessarily observe memory operations in source order. Happens-before, compiler and CPU reordering, barriers, false sharing, cache coherence traffic, safe publication and why double-checked locking is the canonical cautionary tale.
How many workers, and what happens to the work that does not fit?
Pools as bounded resource use rather than unlimited spawning, the reasoning behind sizing (and the refusal to give a universal formula), work stealing, saturation, and bounding concurrency as a first-class design decision.
Which parts of this computation are independent, and what limits the speedup?
Fork/join, parallel reduce, map/reduce as a computational pattern, the overhead that makes parallelising small work slower, Amdahl and Gustafson as complementary intuitions, and work versus span as the real ceiling on a dependency graph.
Is the same operation applied to many items, or different operations at once?
SIMD and data parallelism against task parallelism, pipeline stages that let items occupy different stages simultaneously, fan-out/fan-in, scatter/gather and the tail-latency cost of waiting for the slowest branch.
How do independent tasks agree on when to proceed?
Barriers and latches, `Promise.all` and `gather` and what they do on failure, the sequential-await trap, and the counterweight: parallelising a call fan-out multiplies load on whatever is downstream. Thundering herds and single-flight coalescing.
Who owns this task, and what happens when nobody wants the answer?
Child tasks with a clear lifetime, cancellation as a first-class concern rather than an afterthought, propagation to children, timeouts against deadlines, and the orphaned background task nobody is waiting for and nobody stops.
Can we make coordination unnecessary instead of correct?
The strategies that reduce synchronization rather than perfecting it: immutability, copying versus sharing, copy-on-write snapshots, and optimistic against pessimistic concurrency control with the contention threshold that decides between them.
What shape does this problem already have a known answer for?
The pattern catalogue with problem, structure, trade-offs and failure modes for each — and the anti-patterns that show up in real code review: one global lock, unbounded spawning, locks held across I/O, swallowed task exceptions, and lock-free code written without a reason.
Why is eight cores not eight times faster?
Where the speedup goes: serial fractions, synchronization, memory bandwidth, cache locality destroyed by tasks bouncing between cores, NUMA, affinity, and the numerical fact that parallel reduction can change floating-point results.
The bug disappears when I add a log line. Now what?
Why concurrency bugs resist ordinary debugging, and what works anyway: lock wait metrics, thread and async task dumps, contention profiling, race detectors, deterministic replay, and stress testing designed to shake out schedules that normal runs never produce.
Which concurrency model should this server use?
Thread-per-request against event-driven against hybrid runtimes, UI thread constraints, how databases solve concurrency for data but not for your application memory, and the boundary where a local mutex stops meaning anything at all.
Can these tool calls run at the same time?
Parallel tool execution and what it costs in load, money and determinism; agents racing on the same document; and cancelling a long-running agent run without leaving tool calls, queue entries and side effects behind.
What this domain is not
It is not a list of thread APIs, and it does not re-teach the mechanisms.
Operating Systems teaches you what a mutex is. This domain tells you which region to put it around, what happens in the schedules where you got that wrong, and whether you needed one at all. Where a lesson touches a mechanism — a thread, a semaphore, a context switch, an event loop — it links the Operating Systems lesson and moves straight on to the reasoning. Nothing here asks you to memorize an API surface; every lesson asks what invariant is being protected, which interleavings exist, what the synchronization actually guarantees, and what the concurrency cost.
Concurrency is overlapping progress — a structure in which several pieces of work are all underway. Parallelism is simultaneous execution — more than one thing running at the same instant on different hardware. A system can be concurrent without being parallel, parallel without being concurrent, both, or neither. Almost every confused conversation about performance starts here.
A race condition is a logical bug: correctness depends on timing, and some schedule produces a wrong answer. A data race is a memory-model term: unsynchronized conflicting access to the same location, at least one of them a write. C++ makes a data race undefined behaviour; JavaScript and Python do not have the same rule. You can have a race condition with no data race anywhere — that is the common case in production.
A runtime may schedule thousands of tasks over a handful of threads, or one task per thread, or move a task between threads mid-flight. "Ten thousand concurrent requests" says nothing about how many threads exist. Ask what the unit is, and who schedules it.
The guarantees you program against come from the language: what is atomic, what establishes happens-before, what the compiler may reorder. Underneath, the processor has its own ordering rules. They are related, they are not the same, and reasoning at the wrong level is how "but I wrote it in that order" bugs get shipped.
Every lesson carries required complexity and alternatives fields. A lesson that cannot name the simpler thing to try first is teaching a default rather than a decision — and concurrency is always bought with complexity, so the price goes on the label.
Every schedule, timeline and speedup curve on this site is produced by a model here and is labelled as such. There is no universal ideal thread count, and lock-free is a progress guarantee, not a performance promise.