Two roommates, one shopping list
At the end of the last rung you met the danger that all this sharing hides: two threads reaching into the same heap can step on each other. Now we name it precisely and take it apart. Start with a picture that has nothing to do with computers. Two roommates share one shopping list. Each glances at it, sees there is no milk, and walks to the shop. Each comes home with a carton. Now you have two cartons of milk. Neither person reasoned badly; the trouble was that they both acted on the same shared information at overlapping times, and the outcome depended entirely on how their trips happened to interleave.
That is exactly a race condition: two or more threads touch shared data at overlapping times, and the answer you get depends on the unpredictable order in which their individual steps happen to run. The word 'race' is apt — the threads are unknowingly racing, and who 'wins' each tiny step decides the result. The fix in the kitchen is obvious (write 'GOING FOR MILK' before you leave), and the software fix will rhyme with it: make sure no two threads can act on the shared thing at the same time. But first we have to see why a line as innocent as count++ is even capable of going wrong.
Opening up count++
Here is the surprise that catches almost everyone. You write count++ (or count = count + 1) and read it as a single, instantaneous act. The CPU does not. There is no machine instruction that says 'add one to that memory cell, all in one breath'. Instead the processor performs three separate steps: it reads the value from memory into a register, adds one inside the register, and writes the register back to memory. Three steps, and crucially, the thread can be paused between any two of them — by the scheduler on a single core, or simply because another core is running at the same time.
Now let count hold 5, and let two threads, A and B, each run count++ once. You expect 7. But suppose the steps interleave like this: A reads 5; before A can write, B also reads 5; A adds one and writes 6; B adds one (to the stale 5 it read) and writes 6. Both threads ran flawlessly. Both did 'add one'. Yet the final value is 6, not 7 — one update was silently overwritten and lost. Run those same two threads a thousand times and you will usually get 7, because the bad interleaving is rare. Then, once in a while, you get 6. That intermittent, order-dependent wrongness is the unmistakable signature of a race.
count starts at 5. Goal: two ++ should give 7.
Thread A Thread B count in memory
-------- -------- ---------------
read -> 5 5
read -> 5 5
add -> 6 5
write 6 ------------------------------> 6
add -> 6 (from 5!) 6
write 6 --------------> 6 <-- lost update!
Final: 6, not 7. B's increment overwrote A's.It is non-atomic, not parallel
A common first reaction is: 'that only happens because the two threads run literally at the same time on two cores.' Not so — and this is worth pinning down, because in the last rung we separated concurrency from parallelism. The race needs only concurrency, the mere interleaving of steps, not true parallelism. On a single core with one CPU, the scheduler can pause thread A right after it reads 5, let thread B run its whole read-add-write, then resume A to write its stale 6. One core, no simultaneity, same lost update. Extra cores make the bad timing more likely and more frequent, but they do not create the bug; the non-atomic three steps do.
So the true root cause has a name: a failure of atomicity. An operation is atomic if, to every other thread, it has either not happened at all or has completely finished — there is no observable in-between state. Flipping a light switch is atomic: from across the room the light is on or off, never 'halfway'. If count++ were atomic, no thread could ever sneak in between its read and its write, and the race simply could not arise. The whole trouble is that the read, the add, and the write together are emphatically NOT atomic by default, leaving a gap in the middle that another thread can fall into.
Why a real bug is so cruel to catch
Races are not rare academic curiosities; they are common, they are real, and they are brutally hard to find. Here is why. The bug hides behind code that looks obviously correct — count++ could not be simpler. It surfaces only under particular timings, so it passes every test run on your machine and then fails in production on a busier, many-core server. Worst of all, the act of looking for it often makes it vanish: add a print statement to watch what is happening, and the extra delay changes the timing just enough that the bad interleaving stops happening. Such a bug that disappears whenever you observe it is sometimes called a heisenbug, and races are the classic example.
- Pin down the shared mutable state — the variable two or more threads both read and write (here, count).
- Find every code path that touches it; even one unprotected access elsewhere can corrupt everything.
- Decide what must be indivisible — the smallest stretch (the read-add-write) that has to behave as one step.
- Enforce it: make that stretch effectively atomic with a lock, an atomic instruction, or a higher-level construct.
The shape of every cure
Every genuine cure has the same shape: identify the few lines that read and modify the shared data, and make sure no two threads are ever inside those lines at once. That protected stretch has a name we will live with for the rest of this rung — the critical section — and the one-at-a-time guarantee over it is called mutual exclusion. Mutual exclusion is precisely what turns a non-atomic region into an effectively atomic one: thread A finishes its whole read-add-write before B is allowed to begin, so no update can be lost.
How do we obtain mutual exclusion? The rest of this rung walks the ladder of answers, from the simplest to the richest. We will ask what a correct solution must guarantee (mutual exclusion, progress, and bounded waiting), see the hardware's tiny indivisible building blocks like test-and-set and atomic operations, wrap them into a lock you can grab and release, and finally climb to higher-level tools — the semaphore (a tray of restaurant buzzers handed out and returned), the condition variable, and the monitor. Each is just a more convenient way to guarantee the same thing: that the dangerous stretch behaves as one indivisible step.