Β§32.3–32.4Deadlock Bugs … Summary

Part II OSTEP pp. 389–398 Β· ~8 min read

  • circular wait
  • hold-and-wait
  • no preemption
  • lock-free
  • wait-free
  • livelock
  • deadlock avoidance

Non-deadlock bugs were the majority; deadlock is the classic minority β€” old, hard, and worth understanding deeply.

32.3 Deadlock Bugs

Deadlock is a cycle of threads each holding a lock the next one wants. The minimal case (Fig 32.6):

// Thread 1            // Thread 2
lock(L1);              lock(L2);
lock(L2);              lock(L1);

If Thread 1 grabs L1, then a switch lets Thread 2 grab L2, each now waits for the other forever. The dependency graph makes the cycle visible:

Figure 32.7: the deadlock dependency graph. A cycle (T1 β†’ L1 β†’ T2 β†’ L2 β†’ T1) is the signature of deadlock.

Thread 1Lock L1Thread 2Lock L2holdswanted by T2holdswanted by T1

Why do deadlocks happen?

β€œJust grab locks in the same order” sounds easy β€” so why do deadlocks occur? Two reasons: complex dependencies (the VM system calls the file system to page in a block; the FS calls the VM system for a page β€” a natural circular dependency), and encapsulation. Hiding implementation makes modular software, but hides locking. Java’s Vector.AddAll(v2) internally locks v1 then v2; a concurrent v2.AddAll(v1) locks v2 then v1 β€” deadlock, invisible to the caller.

The four conditions β€” and how to break each

Deadlock needs all four Coffman conditions to hold at once β€” mutual exclusion, hold-and-wait , no preemption , and circular wait . Break any one and it can’t happen β€” giving four families of prevention:

The four Coffman conditions β€” ALL must hold for deadlock. Break any one to prevent it. Click a row.
what it meansbreak it by…the catch
Mutual exclusionthreads claim exclusive control of a resource (a lock)avoid locks β€” lock-free / wait-free via atomic CAShard to build; livelock can still occur
Hold-and-waithold acquired locks WHILE waiting to acquire moreacquire ALL locks at once, atomicallymust know all locks up front; hurts modularity + concurrency
No preemptionlocks can't be forcibly taken from the thread holding themtrylock: grab L1, trylock(L2); on failure release L1 and retrylivelock β€” threads retry in lockstep; add random back-off
Circular waita cycle of threads, each holding what the next wantstotal (or partial) lock ordering β€” always acquire L1 before L2needs deep code knowledge; one slip β†’ deadlock
Dotted-underlined cells have explanations β€” click one.

Circular wait prevention (lock ordering) is the most practical. When a function takes two locks, enforce order by lock address:

// grab in a consistent order regardless of argument order
if (m1 > m2) { lock(m1); lock(m2); } // high-to-low
else         { lock(m2); lock(m1); }
// assumes m1 != m2

No preemption via trylock avoids holding-while-waiting β€” but risks livelock :

top:
    lock(L1);
    if (trylock(L2) != 0) {
        unlock(L1);
        goto top;   // two threads can loop here in lockstep forever
    }

Two threads can repeat this in sync, never progressing (not blocked β€” just spinning fruitlessly). The fix: a random back-off before retrying, breaking the symmetry.

Beyond locks: lock-free structures

To break mutual exclusion itself, use lock-free structures built on atomic compare-and-swap instead of locks. A list insert:

void insert(int value) {
    node_t *n = malloc(sizeof(node_t));
    n->value = value;
    do {
        n->next = head;
    } while (CompareAndSwap(&head, n->next, n) == 0);
}

Step through two threads racing to insert β€” the loser just retries:

Lock-free list insert with compare-and-swap: no lock, no deadlock. A losing racer simply retries β€” never corrupts.
head β†’ANULL

1Both threads read head = A

The list is head β†’ A. Two threads want to insert. Each sets its new node's next = head (= A). insert: do { n->next = head; } while (CAS(&head, n->next, n) == 0);

step 1 / 4

No lock is held, so no deadlock (livelock is still possible). A wait-free structure goes further β€” guaranteeing every operation finishes in a finite number of steps β€” but is harder to build; a few now ship in real systems, including Linux.

Avoidance and detection

Deadlock avoidance uses global knowledge of which locks threads need to schedule around trouble β€” never co-running two threads that could form a cycle:

Deadlock avoidance via scheduling: given each thread's lock needs, never co-schedule two that could form a cycle.
T1T2T3T4
needs L1?yesyesnono
needs L2?yesyesyesno
safe scheduleCPU 2CPU 2CPU 1CPU 1
Dotted-underlined cells have explanations β€” click one.

Since only T1 and T2 grab both locks, keeping them off the same CPU (or never concurrent) guarantees safety; T3 (one lock) and T4 (none) can overlap freely. Dijkstra’s Banker’s Algorithm is the classic version β€” but it needs full advance knowledge and limits concurrency, so it’s rarely general-purpose.

The last resort: detect and recover. Let deadlock happen rarely, run a detector that builds the resource graph and looks for cycles, and restart when one is found (many databases do exactly this).

Tip: Don’t Always Do It Perfectly (Tom West’s Law)

β€œNot everything worth doing is worth doing well.” If a bad thing happens rarely and cheaply, don’t spend enormous effort preventing it β€” a once-a-year reboot may beat a heroic deadlock-avoidance scheme. (If you’re building a space shuttle, ignore this advice.)

32.4 Summary

Non-deadlock bugs β€” atomicity and order violations β€” are common but often easy to fix. Deadlock is old and hard; the best practice is careful lock ordering to prevent it. Lock-free and wait-free approaches, and lock-free programming models like MapReduce, point beyond locks β€” because locks are problematic by nature, and perhaps best avoided unless truly necessary.

Check yourself: deadlock and its remedies

1.All four Coffman conditions must hold for deadlock. Which is the MOST practical one to break, and how?

2.A function grabs two locks passed as arguments. Why lock them in address order (e.g. grab the higher-addressed lock first)?

3.The trylock approach (grab L1; if trylock(L2) fails, release L1 and retry) avoids hold-and-wait. What new problem can it cause?

4.In the lock-free list insert, a thread's CompareAndSwap(&head, n->next, n) returns 0 (failure). What happened, and what does the thread do?

5.How does deadlock AVOIDANCE (as opposed to prevention) work?

6.What distinguishes a wait-free structure from a merely lock-free one?

6 questions