Concurrency
Doing many things at once — why it's powerful, why it's hard, and how to stay sane.
- Distinguish concurrency from parallelism
- Explain why shared mutable state is the core difficulty
- Recognise common patterns for managing concurrent work
So far we've imagined one instruction after another. But real systems do many things at once — handling thousands of users, waiting on the network while still responding. Concurrency is how we structure programs that juggle multiple tasks. It's enormously useful and famously error-prone, so a clear mental model matters.
Concurrency vs parallelism
These get conflated; the distinction is worth keeping:
- Concurrency is dealing with many things at once — structuring your program so multiple tasks are in progress, taking turns. A single chef working several dishes, switching between them.
- Parallelism is doing many things at literally the same instant — multiple CPU cores executing simultaneously. Several chefs, each on their own dish.
A single-core machine can be concurrent (interleaving tasks) without being parallel. The skills overlap but the words aren't synonyms.
The hard part: shared mutable state
Concurrency itself isn't the difficulty — shared mutable state is. When two tasks read and change the same data at unpredictable times, you get a race condition: the result depends on who happens to go first, so the program works 99 times and corrupts data on the 100th. These bugs are maddening because they're intermittent and hard to reproduce.
Recall the systems-thinking lesson: pure code (no shared state, no side effects) is trivially safe to run concurrently — there's nothing to race over. The danger lives entirely where tasks share and mutate the same state.
Patterns for staying sane
The field's hard-won wisdom is mostly about taming shared state:
- Don't share mutable state. The cleanest fix: give each task its own data, or only share data that never changes.
- Message passing. Instead of sharing memory, tasks send messages to each other (the model behind async APIs, and languages like Go and Erlang).
- Queues. Funnel work through a single ordered line so it's processed one at a time, removing the race.
- Locks. When you must share, a lock ensures only one task touches the data at a time — powerful but easy to misuse (deadlocks), so a last resort.
When you don't need concurrency, don't reach for it. The simplest correct program is sequential; add concurrency only where it earns its complexity, and prefer designs that avoid shared mutable state.
Where to go next
With all this complexity, one question dominates: how do you know your system is correct? That's testing and verification, next.