Why one line of C is not one step
In the concurrency rung you met the ugly truth: when two threads share one address space, they share every global and every heap object, and the hardware lets them run at genuinely the same time on different cores. That is exactly when a data race bites. The classic example looks utterly harmless. Take a shared `int counter` and run `counter = counter + 1` from two threads. You would expect the count to go up by two. Often it does. Sometimes it goes up by one, and you have just watched an update vanish into thin air.
The reason is that `counter = counter + 1` is not one step. The CPU cannot add to memory in a single thought; it must load the current value into a register, add one, and store the result back. That three-part dance is a read-modify-write, and it is where the gap opens. If thread A loads 5, then thread B loads 5 before A stores, both compute 6, both write 6, and one increment is lost forever. Nothing is broken in either thread's logic — the bug lives entirely in the interleaving.
Trace one bad interleaving out loud. Thread A loads `r = 5`. Before A can store, the scheduler switches to thread B, which loads `r = 5` too. A adds and stores `counter = 6`. B, still holding its stale 5, adds and stores `counter = 6` again. Two increments ran, yet the counter only moved from 5 to 6 — the second write clobbered the first. The values 5 and 6 are fine; the order in which the loads and stores happened to interleave is the whole disease.
Naming the danger: the critical section
The fix begins by giving the dangerous stretch of code a name. A critical section is any sequence of instructions that touches shared mutable state in a way that must not be interrupted by another thread doing the same thing. In the counter example, the whole load-add-store is one critical section. The goal is not to make those three instructions un-interruptible — the OS can still pause your thread mid-way — but to guarantee that no other thread is running its own copy of that section at the same time.
That guarantee has a name too: mutual exclusion. "Mutually exclusive" means at most one thread may be inside the critical section at any instant; while one is in, all others that want in must wait. Picture a single-occupancy washroom with one key. Anyone can wait outside; only the person holding the key is inside; when they come out they hand the key on. The key does not make you faster, and it does not stop the building from existing — it just enforces one at a time for that one room.
The mutex: a lock with an owner
The tool that enforces mutual exclusion is the mutex — short for *mut*ual *ex*clusion. It is the key from the washroom picture, made real in software. A mutex has just two states, unlocked and locked, plus a notion of who holds it. You wrap your critical section between a lock and an unlock, and the mutex does the rest: the first thread to call lock walks in; any other thread that calls lock while it is held is put to sleep by the OS until the owner unlocks. With POSIX threads the calls are pthread_mutex_lock() and pthread_mutex_unlock().
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void bump(void) {
pthread_mutex_lock(&m); /* enter critical section */
counter = counter + 1; /* now safe: we hold m */
pthread_mutex_unlock(&m); /* leave; wake a waiter */
}Notice what the lock actually buys you. While a thread holds m, it may freely break the data and then fix it — for instance, pop a node off a list, which momentarily leaves the list in a half-edited state — as long as everything is consistent again before it unlocks. That promise is the lock invariant: the rule that holds whenever the lock is free, may be temporarily violated while it is held, and must be restored before unlocking. The lock is what lets you have those private, messy in-between moments without anyone else seeing them.
Spin or sleep? Two ways to wait
When the lock is already held, the waiting thread has two honest options, and they make very different trade-offs. A real mutex puts the waiter to sleep: the OS removes it from the run queue and only wakes it when the lock frees, so a blocked thread burns no CPU. The alternative is a spinlock, which does the opposite — it sits in a tight loop hammering "is it free yet? is it free yet?" until it grabs the lock. Spinning wastes cycles, but it skips the cost of a context switch into and out of sleep.
So the rule of thumb is about how long you expect to wait. If the critical section is a few instructions and held for nanoseconds, spinning often wins, because a context switch can cost more than the whole wait. If the section can be held for a long time — or worse, if the holder might block on I/O — spinning is a disaster: you would burn a whole core doing nothing while the holder sleeps. Spinlocks shine inside the kernel and in carefully measured hot paths; for ordinary application code, reach for a plain mutex first and let it sleep.
What a mutex does not do
Be honest about the limits, because a half-believed lock is more dangerous than none. A mutex protects whatever you agree it protects — and that agreement lives only in your head and your code, never in the language. The compiler will not stop another function from touching `counter` without taking m. A mutex is a convention enforced by everyone choosing to lock the same mutex around the same data. Pick one lock per piece of shared state, document it, and route every access through it.
A mutex also does not, by itself, let you wait for a condition — say, "wait until the queue is non-empty." You could lock, peek, unlock, and loop, but that busy-waiting wastes CPU and fights the lock. The clean answer is a separate tool, the condition variable, which is exactly what the next guide builds. And mutexes are not the cheapest possible tool: for a single counter, a lock-free atomic operation can do the increment in one indivisible hardware step with no lock at all. Atomics are a real alternative for tiny updates — we will meet them at the end of this rung — but they do not scale to protecting a multi-step invariant the way a mutex does.