Skip to article
ALGORITHMICSConcurrency
Concurrency7 min read

Mutexes and Locks

One holder at a time — and why the lost update happens on the line you thought was atomic.


Two threads run count++ on a counter that starts at zero. Afterwards, count is 1.

Not sometimes. Not on a weird machine. This is the ordinary behaviour of the ordinary code, and it happens because count++ is not one operation.

Three operations wearing one costume

read count → register
add 1 → register
write register → count

Two threads can be part-way through those three steps at the same time. Step through both interleavings:

Interleaving
1 / 6

thread A

0

count in memory

0

thread B

  1. A: read count
  2. B: read count
  3. A: add 1
  4. B: add 1
  5. A: write count
  6. B: write count

Both threads read 0 before either wrote. Two increments, and count ends at 1 — one update was silently lost. Nothing crashed and no test necessarily catches it.

Both threads read 0. Both compute 1. Both write 1. One increment vanished, no error was raised, and the “lucky” ordering is the one you will see every time you run it by hand.

The mutex

A lock that exactly one thread can hold. Everyone else waits.

mutex.lock();
try {
count += 1; // nobody else is in here
} finally {
mutex.unlock(); // ← even if the body throws
}

The three steps still happen, but no other thread can begin its own three steps until this one finishes. The region between lock and unlock is the critical section.

Locking has a cost, and it is not the one you expect

An uncontended lock is cheap — often a single atomic instruction. The expense arrives with contention: when a thread has to wait, the OS may deschedule it, and getting it running again costs microseconds.

That is why the goal is not “fewer locks” but shorter critical sections:

mutex.lock();
const result = expensiveComputation(input); // nobody else can work
cache.set(key, result);
mutex.unlock();
const result = expensiveComputation(input); // outside — parallel again
mutex.lock();
cache.set(key, result);
mutex.unlock();

Same correctness, and the second version lets every thread compute at once.

The failure modes

Deadlock. Two threads each holding what the other wants. See deadlock — the fix is a global lock ordering.

Livelock. Threads politely backing off in lockstep and never progressing. Busy, and making no headway.

Starvation. A thread that never gets the lock because others keep taking it. A fair mutex queues waiters in order and prevents this, at some throughput cost.

Priority inversion. A low-priority thread holds a lock a high-priority thread needs. This is not academic: it nearly ended the Mars Pathfinder mission in 1997.

The variants worth knowing

Read-write lock. Many concurrent readers, or one writer. Worth it only when reads heavily outnumber writes — the bookkeeping is more expensive than a plain mutex, so under a balanced load it is slower.

Spinlock. Busy-waits instead of sleeping. Correct only for critical sections shorter than a context switch, and actively harmful in userspace, where the holder can be descheduled while you spin.

Reentrant mutex. The same thread can lock it twice. Convenient, and it hides a design problem — if you need it, some function is calling another that locks the same thing, and that call graph is where deadlocks come from.

Or do not share at all

The reliable way to avoid all of this is to remove the shared mutable state:

Locks are the general answer. Not needing one is the better answer.