Picture a single-occupancy café toilet with one door and one latch. Anyone can walk up, but the moment someone steps in and slides the latch, the little sign flips to OCCUPIED. The next person tries the handle, sees it's taken, and waits. When the occupant slides the latch back to FREE and leaves, exactly one waiting person goes in. One door, one latch, one occupant at a time — nobody ever collides inside.
That latch is a mutex (short for mutual exclusion), and it is the
primary cure for the
acquire() (also called lock()) and
release() (also called unlock()) — that guarantees only
one thread at a time may run the dangerous stretch of code in between.
This page is about that one idea — a lock enforcing mutual exclusion over a critical section — explored from the door-latch down to the atomic instruction that makes it real.
The stretch of code that touches shared data — reading and writing count, pushing to a
shared list, updating a bank balance — is the critical section. The whole danger of
concurrency lives there: if two threads are inside it at the same time, their read–modify–write steps
can interleave and one update vanishes. A lock's single job is to make that impossible.
Mutual exclusion is the guarantee: at most one thread is inside the critical
section at a time. The lock is the mechanism that delivers it. To enter the critical section a
thread must first hold the lock; while it holds the lock, every other thread that tries to
acquire() is made to wait. When the holder release()s, one waiter is let
through. Enter with the key, do your work, hand the key back.
acquire() cannot succeed until the
current holder releases.
Below is the whole life of a lock as a state machine. There are only two states —
FREE and HELD — joined by two transitions: acquire()
flips FREE → HELD, and release() flips HELD → FREE. Press play to watch one thread take
the lock, a second thread block and join the wait queue, and the release wake it.
This is the feature that makes a mutex a mutex and not just any counter: it has an
owner. The thread that calls acquire() is recorded as the holder, and
only that same thread is allowed to call release(). You can't lock a
door for someone else to unlock — the person who slid the latch is the one who slides it back.
Ownership buys two things. It catches bugs — releasing a lock you never acquired is a programming error the runtime can detect and reject. And it enables reentrancy (below): because the lock knows who holds it, it can tell "the owner is asking again" apart from "a different thread is asking". This is exactly what a semaphore does not have — a semaphore is ownerless, and any thread may signal it. If you only want a lock, reach for a mutex, not a semaphore.
What if a function that holds the lock calls another function that also tries to acquire the same lock? With a plain mutex, the thread would block waiting for… itself. Frozen forever. A reentrant (or recursive) mutex fixes this by keeping a hold count alongside the owner: when the owning thread acquires again, the count just goes up (1 → 2 → 3), and it only truly frees the lock when the count returns to zero. So the golden rule becomes: a reentrant lock must be released exactly as many times as it was acquired. Handy for recursive code, but many experts avoid reentrant locks precisely because that "it's fine to re-lock" habit hides sloppy design.
A lock is itself just a variable in shared memory — held = true. But setting that flag
is its own read–modify–write, so it can race exactly like the data it's meant to protect. You cannot
build mutual exclusion out of ordinary reads and writes. The hardware rescues us with
atomic instructions — acquire() is a loop around one
atomic operation.
Because exactly one thread can win the atomic testAndSet, exactly one thread gets past
the while loop — everyone else keeps looping. That is mutual exclusion, bootstrapped from
a single instruction the CPU promises is indivisible.
The acquire() above busy-waits: it spins in a tight loop, burning CPU
cycles doing nothing but checking again. That is a spinlock. It sounds wasteful — and
it is, if the wait is long — but it is exactly right when the critical section is very short:
spinning for a few hundred nanoseconds is far cheaper than the thousands of cycles it costs to put a
thread to sleep and wake it again. Spinlocks are also the only option in places where you
can't sleep at all — inside an operating-system interrupt handler, for instance.
A blocking mutex takes the opposite bet. When it can't get the lock, instead of spinning it asks the scheduler to put the thread to sleep and hand the CPU to someone useful; when the owner releases, the scheduler wakes one sleeper. That wake-up costs real time, but the CPU wasn't wasted meanwhile. This is the right default when a critical section might be held for a while.
Two matching mistakes. First, spinning while holding a lock for a long time is a double crime: you waste your own core doing work slowly and every waiter wastes its core spinning behind you. Second, calling anything that might block or sleep — I/O, a network read, another blocking lock — while inside a spinlock defeats the entire point: the whole idea of a spinlock is "I'll be done in a flash," so a spinner that naps starves every other core waiting on it. Keep spinlock critical sections tiny and non-blocking.
The sandbox runs on one thread, so it can't produce a real race — which is perfect, because
we can script the exact interleaving and make the bug happen every time. A tiny
Mutex class (a held flag plus an owner) guards a shared counter.
We run the worst interleaving both without the lock and with it, and print the results. Press
Run ▶:
Same two increments, same shared counter — the only difference is that the lock forbids the
overlapping schedule. The unsynchronised run loses an update and ends at
A lock's guarantee has a price. If a critical section can hold only one thread, then while one thread is inside, all the others are waiting — the work is serialized. When many threads pile up on the same lock, that's contention, and it can quietly erase the speed-up you bought eight cores for: eight threads sharing one hot lock spend most of their time queued behind it, not computing.
The tuning knob is granularity:
The engineering trade-off is real: coarse locking is correct and slow; fine-grained locking is fast and fiddly. The best lock is one held so briefly that contention rarely arises in the first place — which brings us to the golden rules.
try … finally (or a scope-guard / with block) so the release cannot be
skipped.
The most common lock bug isn't exotic: it's an early return or a thrown exception in the
middle of a critical section that skips right past the release(). The lock is now held
forever, and every other thread that ever wants it blocks permanently — a frozen
program with no crash to point at. The cure is to bind the release to the scope, not to a line you
hope executes:
Never write a bare acquire() … release() pair with real logic between them
unless nothing in that logic can throw or return early. When in doubt, use finally.
Sometimes — with lock-free (optimistic) concurrency. Instead of locking, a thread reads a value, computes its new version, and uses a single compare-and-swap to install it only if nobody changed it meanwhile. If someone did, the CAS fails and the thread simply retries. No thread ever blocks another, so there's no contention bottleneck and no deadlock — at the cost of trickier code and wasted retries under heavy contention. Beware its evil twin, the double-checked locking pattern (check a flag, then lock, then check again): it looks clever for lazy initialisation but is subtly broken without the right memory barriers, and has burned generations of programmers. Lock-free code is powerful and famously hard to get right — a whole field of its own.
Deadlock. Thread A locks mutex 1 then waits for mutex 2; thread B locks mutex 2 then waits for mutex 1. Each holds what the other needs, and both wait forever. Nothing is racing now — the program has simply frozen solid. The cure (acquire locks in a fixed global order, and a few related tricks) is a big enough topic to earn its own page. Locks are the primary cure for race conditions; using them carelessly invites the deadlock.