One cook, many pots: the picture to hold
You arrive at this rung already knowing how a single program runs: one stream of instructions, fetched and executed in order, marching down through main(). Everything so far has been about that single marching line. Now we widen the world — programs that are doing more than one thing at once — and the very first thing to get straight is what "at once" even means. Because there are two completely different things it can mean, and the words for them, concurrency and parallelism, are constantly swapped by accident. Untangling them now will save you from a fog that confuses people for years.
Picture a single cook in a kitchen with three pots on the stove. She is not standing frozen in front of one pot waiting for it to boil. She starts the rice, then while it simmers she chops vegetables, then she stirs the soup, then back to the rice. At any single instant her two hands are doing exactly one thing — there is only one of her — yet over the span of the evening all three dishes are making progress together. That is concurrency: a way of structuring work so that several tasks are in flight at the same time, interleaved, even though only one is actually being touched at any given moment.
Now add a second cook beside her, working a second stove. The instant both knives come down on two cutting boards, two tasks are advancing in the same physical moment — not interleaved, but genuinely simultaneous. That is parallelism: actually doing two things at the same time, which requires two doers. The cleanest one-line way to hold the distinction: concurrency is about dealing with many things at once; parallelism is about doing many things at once. One is a way of organizing; the other is a fact about the hardware underneath.
It comes down to the cores
Strip away the kitchen and what is left is the CPU. A processor core is one cook — one piece of hardware that fetches and executes instructions, one at a time. A machine with a single core can only ever run one instruction stream at any true instant. So how did your single-core laptop from years ago run a browser, a music player, and a download all "at the same time"? It didn't, not literally. The operating system let each one run for a sliver of time — a few milliseconds — then froze it, saved its state, and let the next one run. Swap fast enough and the illusion of simultaneity is seamless to a human.
That freezing-and-swapping is a context switch, and the rapid-fire taking-turns it produces is exactly concurrency on a single core: tasks overlap in wall-clock time without ever overlapping in the same instant. You already met its cousin in the OS rung — this is the scheduler deciding who runs next. So concurrency does not need extra hardware. One core, taking turns fast, gives you concurrency. What it cannot give you is real parallelism, because there is still only one cook.
Real parallelism needs more cores — more cooks. A modern chip has many cores, often 8 or 16, each a full execution unit running its own instruction stream genuinely simultaneously. With four cores, four tasks can advance in the very same nanosecond, no taking turns required. And the two ideas stack: a laptop with 8 cores running 100 tasks is parallel across those 8 cores and concurrent in how those 100 tasks are dealt out and time-sliced onto them. Parallelism is the hardware multiplying your hands; concurrency is the structure that keeps many tasks in flight however many hands you have.
Why bother? Two very different reasons
People reach for concurrency and parallelism for reasons that are worth keeping separate, because they pull in different directions. The first reason is throughput: you have a pile of independent work and you want it finished sooner. Resize 10000 images, sum a giant array, render a frame — split it across cores and, with N cores, you might approach an N-times speedup. This is the parallelism payoff, and it is why "just add more threads" is tempting. But it has a ceiling: only the genuinely independent part of the work speeds up, and the cores you do not have do not help.
The second reason has nothing to do with going faster: it is responsiveness and waiting well. A program often spends most of its life blocked — waiting on a file read, a network reply, a key press. A single-threaded program that calls a blocking read() simply freezes until the bytes arrive; the whole UI goes dead. With concurrency, while one task sits parked waiting for the disk, another can run. This is pure concurrency — it helps even on one core, because the win is not doing more work per second but never standing idle with useful work available. A chat client that stays smooth while downloading a file is enjoying this, not parallelism.
How a program splits into several streams
So a program does more than one thing at once — but what, concretely, is the second "thing"? Up to now your program has had exactly one instruction stream. To be concurrent it needs more than one, and there are two ways to get them, which are the subject of the very next guide. One is to run several separate processes, each its own program with its own private memory, the way the shell runs many programs side by side. The other, lighter way is to spawn several threads inside one process.
A thread is a single independent stream of execution: its own instruction pointer marching through code, and crucially its own stack so it can call functions and hold local variables without trampling anyone else. A normal program is just a process with one thread. Make a second thread and now two streams run inside the same process — and on a multi-core machine the OS can place them on different cores and run them truly in parallel. The difference between threads and processes is the whole next guide; for now just hold that a thread is one more marching line of instructions, and you can have several.
Here is the fork in the road that defines the rest of this rung. Separate processes each get their own private memory, so they cannot accidentally step on each other's data — but talking between them takes deliberate effort. Threads inside one process are the opposite: they are cheap, and they share the process's memory — the same heap, the same global variables, all of it. That sharing is gloriously convenient, and it is also where every hard problem in this rung is born. The moment two streams can touch the same bytes, shared mutable state appears.
Where the trouble lives: shared mutable state
Let two threads share one variable and let both write to it, and you have stepped onto the rake that this whole rung is about. Take the most innocent line imaginable — a shared counter, and both threads run `count++`. It looks atomic, a single indivisible bump. It is not. To the CPU, `count++` is three steps: load the current value from memory into a register, add one to the register, store the register back to memory. Three instructions, and the OS is free to pause a thread between any two of them.
count starts at 5. Two threads each want to do count++.
If their three steps DO NOT overlap, you get 7 (correct):
Thread A: load 5 -> reg=5
Thread A: add 1 -> reg=6
Thread A: store -> count=6
Thread B: load 6 -> reg=6
Thread B: add 1 -> reg=7
Thread B: store -> count=7 result: 7 (good)
But the scheduler may INTERLEAVE the steps like this:
Thread A: load 5 -> reg=5
Thread B: load 5 -> reg=5 (B read the SAME old 5)
Thread A: add 1 -> reg=6
Thread A: store -> count=6
Thread B: add 1 -> reg=6
Thread B: store -> count=6 result: 6 (one ++ vanished!)Read the bad path again: both threads load 5, both add to get 6, both store 6 — two increments happened, but the counter only moved by one. One update was silently swallowed. This is a race condition: the answer depends on the exact, unpredictable interleaving of two streams, and the program will quietly give wrong results some fraction of the time. The deeper name for this specific overlap of unsynchronized reads-and-writes to shared memory is a data race. We have only sketched it here; the fourth guide of this rung, on the race condition, takes `count++` fully apart, and the fifth shows the cure.
And this is the honest, uncomfortable heart of the whole rung: concurrency makes your program nondeterministic. Run the racy counter a thousand times and it may print the right total 999 times and a wrong one once — or be perfect on your laptop and fail only on the server, only under load, only at 3 a.m. The bug does not live in any one line you can point at; it lives in the timing between lines, which changes every run. This is why concurrency is nondeterministic and why these bugs are the hardest most programmers ever meet. The good news: the fix is learnable, and the next four guides build it one careful step at a time.
What you carry into the rest of the rung
Hold the shape of the whole rung now, because every later guide hangs off this one. Concurrency is structure — overlapping tasks in time, useful even on one core, mostly for waiting well; parallelism is hardware — real simultaneity, needing many cores, mostly for going fast. You get multiple instruction streams either as separate processes or as cheap, memory-sharing threads — the next guide, threads versus processes, draws that line. Threads share memory, and shared mutable state under preemptive scheduling is where races are born, because the OS can pause any thread mid-instruction-sequence.
If only one sentence survives, let it be this: concurrency is about dealing with lots of things at once; parallelism is about doing lots of things at once. Everything else in this rung is the careful working-out of what happens when those overlapping streams reach for the same bytes — and how, with locks and atomics and the discipline still to come, we tame the nondeterminism instead of being ambushed by it. Onward to threads versus processes, where the second stream finally gets a name and a stack of its own.