Module 2 · Unit 7 · Discussion 7
Designing a Deadlock-Free Protocol
Deadlock is not a bug that you find by testing; it is a property of the design that either holds or does not. Once a system deadlocks in production, the recovery is expensive — a process is killed, a transaction is rolled back, a service is restarted. The right response is to design the resource acquisition protocol so that deadlock cannot occur. This discussion asks you to do exactly that: given a realistic module with several locks, design a deadlock-free protocol and argue rigorously that your design satisfies the four necessary conditions' negation.
01 Overview
| Course | COMP 3150 / CMIS 3150 — Operating Systems |
|---|---|
| Unit | Unit 7 — Deadlocks |
| Weight | Part of the 5% participation grade (each discussion is worth approximately 0.4% of the final grade) |
| Opens | Monday of Week 7 |
| Initial post due | Thursday of Week 7, 23:59 |
| Peer replies due | Sunday of Week 7, 23:59 |
| Format | One initial post (500–700 words, including a diagram or a table of the lock ordering) and at least two substantive peer replies (150–250 words each) |
| CLOs assessed | CLO3, CLO6, CLO8 |
02 Learning outcomes addressed
By participating in this discussion you will
- Explain the four necessary conditions for deadlock and the meaning of each (U7.1, U7.2).
- Construct and interpret a resource allocation graph and identify the cycles it contains (U7.3).
- Apply the standard prevention and avoidance techniques and evaluate their costs (U7.3, U7.4).
- Design a resource-acquisition protocol that is provably deadlock-free (U7.3, U7.4).
- Evaluate the trade-offs between prevention, avoidance, detection, and recovery (U7.5).
This discussion assesses CLO3 (analyse deadlock solutions), CLO6 (apply Linux systems programming), and CLO8 (design OS-related components).
03 Discussion prompt
The scenario
You are the lead engineer on a new caching subsystem for a key-value store. The subsystem maintains the following shared data structures, each protected by its own mutex:
-
table_lock— protects the hash table that maps keys to cache entries. -
lru_lock— protects the doubly-linked list that tracks the least-recently-used order of entries. -
stats_lock— protects a small structure of hit and miss counters, updated on every lookup. -
flush_lock— protects a queue of entries waiting to be written back to the backing store.
The subsystem must support the following operations, each of which may hold more than one lock at a time:
- get(key) — look up a key in the hash table, move the entry to the front of the LRU list, and increment the hit/miss counter.
- put(key, value) — insert or update an entry, which may require evicting an entry from the LRU list and enqueueing it on the flush queue.
- evict() — remove the least-recently-used entry and enqueue it on the flush queue.
- flush() — drain the flush queue and write each entry to the backing store.
- stats() — read the hit/miss counters.
A junior engineer has implemented the operations with locks acquired in the order that seemed natural, and the subsystem deadlocks under load. Your job is to redesign the lock-acquisition protocol so that deadlock cannot occur, while keeping the subsystem's performance acceptable.
Your task
- Propose a global lock order. Assign a numeric rank to each lock and state the rule that all callers must follow. Draw the resulting lock hierarchy — a small table or a diagram is fine.
- Apply the order to each operation. For each of the five operations, list the locks it acquires, in the order your rule prescribes, and the locks it releases. If an operation needs to acquire a lock with a lower rank than one it already holds, explain how you restructure it to avoid the violation (for example, by releasing and re-acquiring, by using a separate "acquisition phase" and "work phase", or by splitting the operation).
-
Argue correctness. Explain why
your protocol makes deadlock impossible. Your
argument should refer to the four necessary
conditions and show that at least one of them
cannot hold. Specifically:
- If you choose to break circular wait, show that no cycle can form in the wait-for graph because every thread acquires locks in strictly increasing rank order.
- If you choose to break hold-and-wait instead, explain the "acquire all or none" mechanism and its cost.
- If you choose to use trylock with back-off, explain why no cycle persists indefinitely.
-
Evaluate the trade-offs.
Address:
- What is the cost of your protocol in the common case (an uncontended lookup)? In the contended case?
- Does your protocol permit the natural
concurrency of the subsystem (for
example, two
getcalls on different keys happening in parallel)? - Does your protocol introduce the risk of starvation — a thread that waits indefinitely while others proceed? If so, how do you bound it?
- What would you have to give up to simplify the protocol further (for example, by using a single global lock for the whole subsystem)? Is the simplification worth it?
- Compare with an alternative. Discuss one alternative to your protocol — for example, a single global lock, a lock-free design for one of the structures (with atomics and RCU), or a partitioned design with one lock per hash bucket. State what the alternative would gain and what it would cost. State which one you would recommend and why.
Your post should be 500–700 words. Include the lock table or diagram. Cite at least one source (a textbook section, a paper, or a documentation page on lock ordering or on a real system that uses one — for example, the Linux kernel's lockdep, or the lock-ordering documentation of a database).
04 Notes on the techniques
The following notes may help you frame your argument. They are not exhaustive; you are expected to bring your own reasoning.
4.1 Global lock ordering
Assign a unique numeric rank to each lock. Every thread must acquire locks in strictly increasing rank order. Then no cycle can form in the wait-for graph: if thread T1 holds lock A and is waiting for lock B, then rank(A) < rank(B); the next thread in any cycle would need to wait for a lock with rank greater than rank(B), and so on, and the chain of ranks would have to be strictly increasing all the way around — which is impossible for a finite set of locks. This is the standard technique and the one used by most production systems, including the Linux kernel (through lockdep) and PostgreSQL (through documented lock order).
4.2 Try-lock with back-off
Instead of blocking on a lock, a thread can call
pthread_mutex_trylock. If the lock is
held, the thread releases the locks it holds and
retries after a random delay. This breaks the
"no preemption" condition: the resource can be
effectively preempted from the holder's
perspective, because the caller decides not to wait.
The technique avoids deadlock but can livelock if
the retry policy is poor; a bounded number of
retries and a randomised back-off are essential.
4.3 A single global lock
The simplest way to eliminate deadlock is to use a single lock for the entire subsystem. There is never a second lock to acquire, so circular wait cannot form. The cost is concurrency: no two operations can proceed in parallel, and every operation serialises on the global lock. For a subsystem whose operations are short (as a cache lookup typically is), the simplicity is often worth the loss of concurrency. For a subsystem whose operations are long or involve I/O, the global lock becomes a bottleneck.
4.4 Partitioning
A middle ground: partition the hash table into
buckets, each with its own lock. Two
get calls on keys in different buckets
proceed in parallel. But the LRU list is shared
across all buckets, so operations that need to
touch both the bucket lock and the LRU lock must
follow a lock order: always acquire the bucket lock
first, then the LRU lock. (Or the LRU list can be
partitioned too, at the cost of a global view of
LRU order.)
4.5 Lock-free designs
A fully lock-free design avoids deadlock by not using locks at all: the data structures are updated using atomic operations, and readers use read-copy-update (RCU) or hazard pointers to avoid seeing partially updated state. This eliminates the deadlock problem entirely but shifts the complexity to the memory-reclamation strategy and the algorithm design. The Linux kernel uses RCU extensively for read-mostly data structures.
05 Grading rubric
The discussion is marked out of 100 points. The criteria and their weights are given below.
| Criterion | Weight |
|---|---|
| Post quality (protocol design, correctness argument, trade-offs) | 40% |
| Peer response quality | 30% |
| Engagement and timeliness | 20% |
| Netiquette and clarity | 10% |
5.1 Post quality (40 points)
| Level | Descriptor |
|---|---|
| Exemplary (90–100%) | A clear lock order with a table or diagram. Each of the five operations is correctly mapped to the locks it acquires in the prescribed order, with any restructuring explained. The correctness argument is rigorous: it refers to the four conditions and shows that at least one cannot hold. The trade-offs are analysed in detail, including concurrency, starvation, and the common-case cost. An alternative design is discussed fairly and a recommendation is made. A source is cited. |
| Proficient (80–89%) | A clear lock order with a table. Each operation is correctly mapped. The correctness argument is sound but not fully rigorous. The trade-offs are mentioned but not fully analysed. An alternative is discussed but no recommendation is made. |
| Developing (70–79%) | A lock order is proposed but it is not applied correctly to all operations, or it does not cover all the locks. The correctness argument is descriptive rather than rigorous. The trade-offs and the alternative are missing or superficial. |
| Beginning (60–69%) | No coherent lock order. The operations are described but the lock acquisition is not analysed. The correctness argument is absent. |
| Not demonstrated (<60%) | The post is missing, off-topic, or a restatement of the prompt without a design. |
5.2 Peer response quality (30 points)
The descriptors are the same as for Discussion 1. An exemplary reply might:
- Identify a lock-ordering violation the classmate missed (for example, a code path that acquires two locks in the opposite order).
- Propose an alternative lock order and compare its trade-offs with the classmate's.
- Challenge the claim that the protocol is starvation-free and propose a scenario in which a thread could wait indefinitely.
5.3 Engagement and timeliness (20 points)
The descriptors are the same as for Discussion 1.
5.4 Netiquette and clarity (10 points)
The descriptors are the same as for Discussion 1.
06 Resources
- Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 8 (Deadlocks).
- Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. Operating Systems: Three Easy Pieces, Chapter 32 (Concurrency Bugs: Deadlock).
- Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 6 (Deadlock).
- Coffman, E. G., Elphick, M. J., & Shoshani, A. System Deadlocks. ACM Computing Surveys, 1971.
- Havender, J. W. Avoiding Deadlock in Multitasking Systems. IBM Systems Journal, 1968.
- Linux kernel documentation:
Documentation/locking/lockdep-design.rst(the lockdep lock-order validator). - Linux man-pages:
man 3 pthread_mutex_lock,man 3 pthread_mutex_trylock,man 3 pthread_mutexattr_setprotocol(for priority inheritance). - PostgreSQL documentation: Lock Management (documented lock levels and lock ordering).
- Herlihy, M., & Shavit, N. The Art of Multiprocessor Programming, 2nd ed., Chapters 8 and 9 (Monitors and Linked Lists).