From diagnosis to cure
Guide 4 left you with a precise, uncomfortable fact: when two threads run `count++` on the same shared variable, updates can silently vanish. The reason was that `count++` is not one indivisible step — it is a read, then modify, then write: load count into a register, add one, store it back. If thread A reads 41, and thread B reads 41 before A stores its 42, both will store 42, and one increment is simply gone. The variable is shared mutable state, the two threads interleave at unpredictable moments, and the result is a race condition. This guide answers the natural next question: now that we can name the disease, what is the cure?
Notice exactly what went wrong, because the cure is shaped by it. The bug was not that the threads ran in the wrong order — there is no right order; either thread may legitimately win. The bug was that one thread's read-modify-write got cut open halfway through, and the other thread slipped inside the gap. The fix, then, is not to control the order. It is to make sure that once a thread starts its read-modify-write, no other thread can touch count until that thread has finished. We need a way to say: *this little stretch of code must run to completion without interference.* That stretch has a name.
The critical section and the rule of mutual exclusion
A critical section is a region of code that accesses shared state and must not be run by more than one thread at the same time. In our counter, the critical section is the three little instructions that make up `count++`. The promise we need is mutual exclusion: at most one thread may be inside the critical section at any instant; everyone else who wants in must wait their turn outside the door. If that promise holds, the interleaving that lost an update becomes impossible — thread B cannot read count until thread A has fully stored its result, so B reads 42, not the stale 41, and produces 43. No lost update, every time.
Be honest about the cost this idea carries. Mutual exclusion deliberately removes parallelism from the critical section: while one thread is inside, the others sit idle. That is the whole point — but it means a critical section is a place where your concurrent program briefly goes serial, so you want it as small as the correctness rule allows. Lock around the `count++`, not around the entire function that happens to contain it. A common beginner overcorrection, after being burned by a race, is to wrap huge swathes of code in one lock; that is correct but slow, because you have thrown away the concurrency you went to the trouble of creating. The skill is finding the smallest region that still protects the invariant.
The mutex: a lock on the door
The classic mechanism is a mutex — short for mutual exclusion lock. Think of it as a key to a one-person room. Before entering the critical section a thread calls pthread_mutex_lock(); if the room is free it walks in holding the key, and if another thread already holds the key it blocks — the kernel puts it to sleep — until the key is returned. When the thread finishes the critical section it calls pthread_mutex_unlock(), handing the key back so a waiting thread can wake and enter. Because there is exactly one key, at most one thread is ever inside. That is mutual exclusion, made real.
#include <pthread.h>
static long count = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *worker(void *arg) {
(void)arg;
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock); /* enter critical section */
count++; /* the read-modify-write, now protected */
pthread_mutex_unlock(&lock); /* leave; wake a waiter */
}
return NULL;
}
/* Two threads each run worker(); join both; count is now exactly 2000000. */Why can't we fix the race with something weaker — say, a clever ordering of plain reads and writes, with no lock at all? Because the hazard lives below the level of your C statements. The compiler and CPU are both allowed to reorder ordinary memory operations, and a context switch can fall between any two machine instructions; without a synchronization primitive there is no point in the read-modify-write that the system guarantees is indivisible. A mutex works precisely because lock and unlock are special: they are themselves built on hardware atomic operations (and the memory ordering guarantees that come with them), so they cannot be torn apart the way `count++` could. You are not adding more code that might be interrupted; you are reaching down to an operation the hardware promises cannot be.
Atomics: when the lock is a single instruction
There is a lighter cure for the specific case of a lone counter. Modern CPUs offer atomic read-modify-write instructions — a single hardware operation that loads, increments, and stores in one indivisible step that no other core can interrupt. In C you reach it through `_Atomic long count;` and an atomic increment, or in C++ through `std::atomic<long>`; underneath sits an instruction such as a compare-and-swap or an atomic add. For one variable this is faster than a mutex, because the thread never has to sleep or wake — it simply asks the hardware to do the whole read-modify-write at once.
So why keep mutexes at all, if atomics are faster? Because atomics protect one memory location at a time, and most real invariants span several. If you must move money from one account to another, the rule "the two balances always sum to the same total" is a relationship across two variables; no single atomic instruction can keep both consistent through the transfer. A mutex can guard the whole multi-step update as one critical section. The honest rule of thumb: reach for an atomic when the entire shared operation is a single read-modify-write on a single value; reach for a mutex when correctness depends on several operations or several variables staying consistent together — when there is a lock invariant that must hold across the whole critical section.
Locks solve one problem and open another
It would be dishonest to leave you thinking a mutex is a tidy off-switch for all concurrency pain. Synchronization is a genuine new layer of difficulty, and it brings its own failure modes. Forget to unlock on one path out of a function and every other thread waits forever. Worse, take two locks in different orders in different threads — thread 1 holds lock A and wants B while thread 2 holds B and wants A — and both wait for each other eternally. That stalemate is a deadlock, and it is the signature hazard that arrives the moment you have more than one lock. The cure for races is real, but it is not free, and it must be used with care.
Step back and see what you have built across this rung. You learned that concurrency is not parallelism, that a thread is a runner sharing one address space rather than a private process, how to spawn one with pthreads, and that sharing mutable state across threads produces a race condition because `count++` is not indivisible. This guide closed the loop: the cure is a critical section protected by mutual exclusion, enforced by a mutex (or, for a single value, an atomic operation) that rests on hardware guarantees nothing weaker can provide. The next rung builds on this floor — condition variables to wait for a state rather than just exclude others, semaphores, the producer-consumer pattern, and the deadlocks you must learn to avoid. You now have the one idea the rest depends on: shared mutable state needs synchronization, and you know exactly why.