Frontend Labs
⋯

Chapter 18 — Locks, semaphores, and deadlock

In one sentence

A lock lets you turn a multi-step operation into something that behaves as if it were ONE uninterruptible step, at the cost of real risk: two locks taken in inconsistent order can leave everyone waiting on each other, forever.

Why this matters

Locks are the standard fix for Chapter 17's race conditions — and getting them wrong (holding them too long, acquiring them in inconsistent order) creates a different, equally real class of production incident: the deadlock.

How it actually works

   A MUTEX (mutual exclusion lock) is the simplest form: only ONE
   thread/process can HOLD it at a time. Anyone else trying to acquire
   it BLOCKS (waits) until the holder releases it.

     lock(mutex)
       counter += 1     // now genuinely safe: nobody else can be in
                        // here at the same time
     unlock(mutex)

   A SEMAPHORE generalises this: it allows up to N holders at once
   (a mutex is just a semaphore with N=1) -- useful for things like
   "allow at most 5 concurrent connections to this resource."

   DEADLOCK is the classic failure mode of locking: two (or more) threads
   each hold a lock the OTHER one needs, and neither will ever release
   what it has:

     Thread A:  locks Resource1, then tries to lock Resource2 (blocked --
                B has it)
     Thread B:  locks Resource2, then tries to lock Resource1 (blocked --
                A has it)
     -> both wait FOREVER. Nothing will ever unblock either of them.

   FOUR CONDITIONS must ALL be true for deadlock to occur (the classic
   formulation, from Coffman et al., 1971) -- and breaking ANY ONE of
   them prevents it:
     1. MUTUAL EXCLUSION       resources can't be shared (inherent to
                              locks)
     2. HOLD AND WAIT          a thread holds one resource while waiting
                              for another
     3. NO PREEMPTION          a resource can't be forcibly taken away
     4. CIRCULAR WAIT          a cycle of threads each waiting on the
                              next

   THE MOST COMMON REAL-WORLD FIX: always acquire multiple locks in the
   SAME, CONSISTENT ORDER, everywhere in your codebase (e.g. always lock
   "lower resource ID before higher resource ID") -- this breaks
   condition 4, circular wait, and is simple enough to actually follow
   as a team-wide convention.

Real-world example

   NASA's Mars Pathfinder rover, after landing successfully in July
   1997, began experiencing unexplained total system resets days into
   its mission -- a serious, publicly documented problem for an
   irreplaceable spacecraft millions of miles from Earth. The cause,
   diagnosed and famously explained afterward by the mission's lead
   developer Glenn Reeves, was a PRIORITY INVERSION bug, a close
   relative of this chapter's deadlock family: a LOW-PRIORITY task
   would occasionally acquire a mutex protecting a shared information
   bus, then get PREEMPTED by MEDIUM-priority tasks (which had nothing
   to do with that mutex at all) before it could finish and release the
   lock. Meanwhile, a HIGH-priority task that actually NEEDED that same
   mutex sat blocked, waiting -- and because the medium-priority tasks
   kept the LOW-priority lock-holder from ever running long enough to
   finish, the high-priority task could be starved long enough to trip
   a watchdog timer, causing the whole system to reset.
   Engineers on Earth DIAGNOSED and FIXED this remotely by enabling
   "priority inheritance" (a real-time-scheduling technique where a
   low-priority task temporarily BORROWS the priority of whoever is
   waiting on its lock, so it can finish and release quickly) --
   uploaded to the rover as a patch, resolving the resets. It remains
   one of the most famous, well-documented real demonstrations that
   locking bugs are not merely academic: they took down a Mars rover,
   and the fix is a named, standard technique specifically because this
   class of problem recurs.

Try it yourself

# Fix the race condition from Chapter 17 with a proper lock, and see
# the difference
cat > lockfix.py << 'EOF'
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:            # now the read-modify-write IS atomic
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(f"expected 400000, got {counter}")   # now always correct
EOF
python3 lockfix.py   # run it several times -- always 400000 now

Common mistakes

  • Holding a lock longer than necessary. Do the minimum work inside the locked section — slow I/O or expensive computation inside a lock turns a small critical section into a bottleneck for every other thread waiting on it.
  • Acquiring locks in inconsistent order across different code paths. This is THE most common real-world cause of production deadlocks — establish and enforce a consistent lock-ordering convention.
  • "My code doesn't have explicit locks, so it can't deadlock." Database row locks, distributed locks (Redis, ZooKeeper), and other implicit locking mechanisms can deadlock exactly the same way — this isn't unique to hand-written mutexes.

Check yourself

  1. What does a mutex guarantee, and what's the difference between a mutex and a semaphore?
  2. List the four conditions required for deadlock, and name one way to break the cycle.
  3. In the Mars Pathfinder case, what specifically caused the high-priority task to be starved, and how was it fixed?

Further reading

  • Book (free): OSTEP, "Locks" and "Condition Variables" chapters.
  • Account: Glenn Reeves' own account/interviews about the Mars Pathfinder priority-inversion bug — search "Mars Pathfinder priority inversion" for the original engineering writeups.
  • Reference: man 7 pthreads, man 3 pthread_mutex_lock.

Generated from real-life-os-guide.md · 8 Oct 2026 16:36