Unit 3 · OS & concurrency · Lesson 7 of 11
Races, deadlocks, starvation and cancellation
Step 1: Learn the ideas
- Race conditions versus data races
- Atomicity violations and order violations
- Check-then-act and read-modify-write races
- Deadlock: the four conditions
- Lock ordering and lock hierarchies
- Try-lock, backoff and livelock
- Starvation, fairness and priority inversion
- Deadlocks beyond locks: thread pools, async runtimes, fork
- Detection: wait-for graphs, thread dumps, race detectors
- Cooperative cancellation, deadlines and cleanup
- Structured concurrency and graceful shutdown
Why it matters
Concurrency bugs pass tests for months and then fail under production load, during a deploy or on a machine with more cores. They are expensive because they are hard to reproduce, and fixes often create new ones: a lock that removes a race introduces a deadlock, and a timeout that breaks the deadlock introduces a livelock. Interviewers use them to see whether you reason systematically: classify the bug, find the invariant that was violated, and choose a fix with known costs. The same thinking applies to async code, agent pipelines and distributed systems, where cancellation and shutdown are frequent sources of leaks and hangs.
Core ideas
Race condition versus data race. A race condition is a semantic bug: the result depends on timing. A data race is a language-level property: two threads access the same location, at least one writes, and nothing orders the accesses. The two overlap but differ. A bank account whose balance() and withdraw() are each locked has no data race, yet still has a race condition if the check and the withdrawal are separate critical sections. A data race in C or C++ is undefined behavior even when it looks harmless; the compiler may hoist a load out of a loop and spin forever.
The two big non-deadlock patterns. Studies of real bugs find that most non-deadlock bugs are atomicity violations, where code assumed a sequence was indivisible (check-then-act, read-modify-write), or order violations, where code assumed one thread's action happened before another's (using an object before its initialization is published). Fix atomicity violations by widening the critical section or using an atomic operation that covers the whole step; fix order violations with an explicit happens-before edge such as a condition variable, a join or a future.
Deadlock requires four conditions at once: mutual exclusion, hold-and-wait, no preemption and circular wait. Break any one:
- Circular wait: impose a global lock order (by rank, or by address for dynamic pairs) and never call unknown code such as callbacks while holding a lock. This is the usual practical answer.
- Hold-and-wait: acquire all needed locks up front, or under one guard lock.
- No preemption: use try-lock and, on failure, release everything and retry. Add randomized backoff, or two threads can retry in lockstep forever, which is livelock.
Databases take another route: they detect cycles in a wait-for graph and abort a victim transaction.
Deadlocks without obvious locks. A bounded thread pool whose tasks block waiting for other tasks queued behind them; an async handler that blocks the event-loop thread on a future that needs that same loop to complete; a child created by fork() in a multithreaded program inheriting a mutex held by a thread that does not exist in the child.
Starvation and priority inversion. Unfair locks, reader-preferring readers-writer locks and strict priorities can starve a thread indefinitely. Priority inversion occurs when a low-priority thread holds a lock that a high-priority thread needs while medium-priority work keeps the holder off the CPU; priority inheritance is the standard fix.
Cancellation. Killing a thread asynchronously can leave locks held and invariants half-updated, which is why asynchronous pthread_cancel is almost never safe. Cooperative cancellation passes a token or deadline down the call chain: blocking operations accept it, code checks it at safe points, and cleanup runs in finally, defer or destructors. Structured concurrency (task groups, nurseries) guarantees that child tasks finish or are cancelled before their parent scope exits, so nothing leaks. Never swallow a cancellation signal, and make cleanup idempotent.
Common misconceptions
- "No data races means no race conditions." Separately locked steps can still interleave badly.
- "A deadlocked process burns CPU." Deadlocked threads usually sleep at zero CPU; high CPU without progress suggests livelock or spinning.
- "Timeouts fix deadlocks." They turn a hang into failures and retries, can cause livelock, and leave the lock-order bug in place.
- "Killing the thread is a clean way to stop work." It skips cleanup and can corrupt shared state; cancel cooperatively.
- "Tests passed, so there are no races." Rare interleavings need race detectors, stress runs and schedule perturbation; absence of failure is weak evidence.
- "Some data races are benign." In C and C++ every data race is undefined behavior; use relaxed atomics if you need an access that looks unsynchronized.
How to approach it
When you meet a bug, first name its class (atomicity, order, deadlock, livelock, starvation, leak on cancellation), then the invariant that broke, then the fix and its cost. For hangs, take two thread dumps a few seconds apart and draw who holds what and who waits for what. Run code under ThreadSanitizer or your language's race detector as a habit. Graduate interviews focus on the deadlock conditions and dining philosophers; mid-level ones on diagnosis and lock ordering in real code; senior ones on cancellation and shutdown design across services and on hangs involving thread pools or async runtimes.
Suggested order
- 1Read OSTEP Ch. 32 (Common Concurrency Problems) for atomicity and order violations and the deadlock conditions with their fixes.
- 2Read the findings sections of the Lu et al. ASPLOS 2008 study to see which bugs and fixes occur in real software.
- 3Do the OSTEP Ch. 27 helgrind homework and the Ch. 32 deadlock homework, and run the same programs under ThreadSanitizer.
- 4Solve The Dining Philosophers two ways (lock ordering and limiting concurrency), then Building H2O.
- 5Read perfbook Ch. 7 (Locking), section 7.1 Staying Alive, for deadlock, livelock, starvation and unfairness in practice.
- 6Read Timeouts and cancellation for humans and its companion essay on structured concurrency, then sketch cancellation for a three-stage pipeline.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
Operating Systems: Three Easy PiecesStart here
University of Wisconsin–Madison (free online book)Section: Ch. 32: Common Concurrency Problems
Primary taxonomy of atomicity and order violations and the four deadlock conditions with prevention strategies.
Open(opens in a new tab)Link responded, 5 Oct 2026Learning from Mistakes: A Comprehensive Study on Real World Concurrency Bug CharacteristicsStart here
Shan Lu's publications page (University of Chicago)Evidence about which concurrency bugs occur in real software and how developers fixed them.
Open(opens in a new tab)Not checked, 5 Oct 2026Is Parallel Programming Hard, And, If So, What Can You Do About It?
kernel.org (author-maintained free book)Section: Ch. 7: Locking
Practical treatment of deadlock, livelock, starvation and lock hierarchies from kernel experience.
Open(opens in a new tab)Link responded, 5 Oct 2026ThreadSanitizer (Clang documentation)
LLVM ProjectHow to run a data-race detector and read its reports.
Open(opens in a new tab)Link responded, 5 Oct 2026Timeouts and cancellation for humans
Nathaniel J. Smith's blog (vorpus.org)Why deadline-based, scoped cancellation composes better than ad hoc timeouts; read with the companion essay on structured concurrency.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Solve The Dining Philosophers twice (a global lock order, then at most four diners at the table) and name the deadlock condition each version breaks. In the OSTEP deadlock homework, time every variant and explain what each avoidance strategy costs. Run the helgrind homework programs under ThreadSanitizer as well and compare the reports. Finally, write a small pipeline (producer, worker pool, consumer) in your main language with deadline-based cancellation: cancel it midway and confirm from logs that every task exits, every lock is released and no item is processed twice.
- The Dining PhilosophersLeetCode(opens in a new tab)Not attemptedLeetCode
- Building H2OLeetCode(opens in a new tab)Not attemptedLeetCode
- OSTEP Ch. 32 homework: deadlock and its avoidance strategiesOpen(opens in a new tab)Not attemptedOSTEP homework
- OSTEP Ch. 27 homework: finding races and deadlocks with helgrindOpen(opens in a new tab)Not attemptedOSTEP homework
- OSTEP Ch. 30 homework: correct and broken producer/consumer queuesOpen(opens in a new tab)Not attemptedOSTEP homework
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1What are the four conditions for deadlock, and which is easiest to break in practice?
- Question 2How can try-lock with retry lead to livelock, and how do you prevent it?
- Question 3What is priority inversion, and how does priority inheritance address it?
- Question 4Why can a fixed-size thread pool deadlock even when the code holds no locks?
- Question 5Why is fork() in a multithreaded program dangerous?
- Question 6How should a function behave when it is cancelled while holding resources?
You’re done when you can
- Give an example of a race condition without a data race and of a data race, and explain both in under two minutes.
- Solve The Dining Philosophers with two different deadlock-prevention strategies and name the condition each one breaks.
- Diagnose a deadlock from a thread dump by drawing the wait-for cycle.
- Explain why asynchronous thread cancellation is unsafe and describe a cooperative design with cleanup.
- Find and fix a seeded data race using a race detector.
Finish the lesson
Unlocks next: Linearizability, concurrent data structures and memory reclamation


