Operating Systems Module 6 – Deadlocks
Preventing deadlocks by design
Make deadlock impossible by ruling out one Coffman condition: one global lock order, all-at-once requests, try-lock with back-off, or enough units.
What you will learn
- Prevent deadlock by denying each Coffman condition, and judge the cost of each
- Apply one global lock order, by rank or by address, to code that takes several locks
- Compute the fewest units of a resource that rule out deadlock
- Choose between try-lock with back-off and taking every lock up front
Before you start
What you’ll learn · Before you start
What you will learn
- Prevent deadlock by denying each Coffman condition, and judge the cost of each
- Apply one global lock order, by rank or by address, to code that takes several locks
- Compute the fewest units of a resource that rule out deadlock
- Choose between try-lock with back-off and taking every lock up front
Before you start
On this page
Eight threads in the program below made 800,000 transfers between 16 accounts, each transfer holding two account locks at once, and none of them ever deadlocked. No lucky timing made that safe: every thread took the lock of the lower-numbered account first, so the circular wait that a deadlock needs could never form.
That is deadlock prevention: Designing a system so that one of the four Coffman conditions can never hold, which makes deadlock impossible whatever the timing.. Every deadlock needs all four Coffman conditions, so a design that makes any one of them impossible can never deadlock, whatever the timing. Each condition can be broken, and each costs something different: locks held before they are needed, work thrown away and redone, or a rule that every line of the program must follow.
One broken condition is enough, and each has a price
Each Coffman condition has its own cure. Sharing the resource removes mutual exclusion: read-only data, or one owner thread that serves every request, leaves nobody waiting for it. Taking every lock up front removes hold and wait: a thread asks for all its locks in one step and holds nothing while it waits. try-lock: A lock call that returns at once and says whether it got the lock, instead of waiting for it; pthread_mutex_trylock and Go's Mutex.TryLock are examples. with back-off breaks no preemption: a thread that cannot get its next lock gives back what it holds and tries again later. One lock ordering: Preventing deadlock by giving every lock a rank and making every thread take the locks it holds together in increasing rank, so that no chain of waits can close into a cycle. removes circular wait: every thread takes its locks in increasing order, so no chain of waits can close.
Dijkstra’s five dining philosophers show the deadlock and one of these cures. When every philosopher picks up the left fork first, all five can hold one fork and wait for the other, the deadly embrace. Taking both forks in one indivisible step removes hold and wait, and Dijkstra set even that aside because a philosopher could then starve. Answer the questions below for a resource of your own:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Way to prevent | Cost at run time | What the code must know | What can still go wrong |
|---|---|---|---|
| Share the resource Rules out: mutual exclusion | Good readers never wait for each other | Mixed only for data that is read, or that one owner thread serves | Mixed most data that changes cannot be shared |
| Take every lock up front Rules out: hold and wait | Poor locks are held long before they are used, so others wait | Poor every lock the thread will need before it starts | Mixed a thread that needs many locks may wait a long time |
| Try-lock and back off Rules out: no preemption | Mixed work done under a lock is redone after each retry | Good nothing in advance | Poor threads that retry in step can livelock |
| One global lock order Rules out: circular wait | Good nothing beyond taking the locks | Mixed one order for every pair of locks that a thread can hold together | Mixed one path that takes the locks the other way brings the risk back |
How the answers decide
- “Can every user of the resource use it at the same time, for example because it is only read?” No, one user at a time: rules out Share the resource. A resource that one user at a time must have keeps mutual exclusion, so sharing is not available for it.
- “Can every user of the resource use it at the same time, for example because it is only read?” Yes, it can be shared: adds a caveat to Take every lock up front, Try-lock and back off and One global lock order. Data that every user can read at once needs no lock to wait for, which beats any rule about how to wait.
- “Does each thread know every lock it will need before it starts?” No, it finds out as it runs: rules out Take every lock up front. A thread cannot ask for all its locks in one step when it learns what it needs only as it runs.
- “Can a thread give back what it holds and safely redo the work it did?” No, it has side effects: rules out Try-lock and back off. Giving a lock back means undoing what was done under it, and a sent message or a written file cannot be taken back.
- “Can all the code that takes these locks agree on one order for them?” Yes, one order fits all of it: adds a caveat to Take every lock up front and Try-lock and back off. Once the code agrees on an order, the order costs nothing at run time, while the other two keep their waiting or their retries.
- “Can all the code that takes these locks agree on one order for them?” No, some of it is not ours: adds a caveat to One global lock order. An order that some code does not follow is broken by the first path that takes the locks the other way round.
Order first
Reach for one global lock order first. It costs nothing at run time and asks only that every path follows it; keep the other three for locks that no single order can cover.
Checkpoint
Check: The four ways to prevent deadlock and their costs
A global lock order makes the cycle impossible
Number every lock, and let each thread take locks only in increasing number. A thread that holds lock i can then wait only for a lock numbered above i. Follow any chain of waiting threads and the numbers keep rising, so the chain can never return to its start: there is no cycle, and so no deadlock. In ordered_locks.c the number is the account number, and the thread locks the two accounts of a transfer from the lower to the higher:
// Eight threads move money between 16 accounts. Every transfer locks its two
// accounts in one global order, the lower account number first, so no cycle of
// waiting threads can ever form.
#include <inttypes.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
enum { ACCOUNTS = 16, THREADS = 8, TRANSFERS = 100000 };
static struct {
pthread_mutex_t lock;
int64_t balance;
} accounts[ACCOUNTS];
typedef struct {
uint64_t seed;
long downwards; // transfers to a lower account number
} worker;
// SplitMix64: the same pseudo-random numbers on every machine.
static uint64_t next_random(uint64_t *state) {
uint64_t z = (*state += 0x9e3779b97f4a7c15u);
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9u;
z = (z ^ (z >> 27)) * 0x94d049bb133111ebu;
return z ^ (z >> 31);
}
static void transfer(int from, int to, int64_t amount) {
const int first = from < to ? from : to; // the global order
const int second = from < to ? to : from;
pthread_mutex_lock(&accounts[first].lock);
pthread_mutex_lock(&accounts[second].lock);
accounts[from].balance -= amount;
accounts[to].balance += amount;
pthread_mutex_unlock(&accounts[second].lock);
pthread_mutex_unlock(&accounts[first].lock);
}
static void *run(void *arg) {
worker *w = arg;
for (int i = 0; i < TRANSFERS; i++) {
const int from = (int)(next_random(&w->seed) % ACCOUNTS);
int to = (int)(next_random(&w->seed) % (ACCOUNTS - 1));
if (to >= from)
to++; // any account but the source
const int64_t amount = (int64_t)(next_random(&w->seed) % 100) + 1;
transfer(from, to, amount);
w->downwards += to < from;
}
return NULL;
}
static int64_t total(void) {
int64_t sum = 0;
for (int i = 0; i < ACCOUNTS; i++)
sum += accounts[i].balance;
return sum;
}
int main(void) {
for (int i = 0; i < ACCOUNTS; i++) {
pthread_mutex_init(&accounts[i].lock, NULL);
accounts[i].balance = 1000000;
}
const int64_t before = total();
pthread_t threads[THREADS];
worker workers[THREADS];
for (int i = 0; i < THREADS; i++) {
workers[i] = (worker){.seed = (uint64_t)i + 1, .downwards = 0};
pthread_create(&threads[i], NULL, run, &workers[i]);
}
long downwards = 0;
for (int i = 0; i < THREADS; i++) {
pthread_join(threads[i], NULL);
downwards += workers[i].downwards;
}
printf("%d threads made %d transfers between %d accounts\n", THREADS,
THREADS * TRANSFERS, ACCOUNTS);
printf("%ld moved money to a lower account number, so the thread locked the "
"destination first\n",
downwards);
printf("total before %" PRId64 ", total after %" PRId64 "\n", before,
total());
return 0;
} Output
8 threads made 800000 transfers between 16 accounts 399757 moved money to a lower account number, so the thread locked the destination first total before 16000000, total after 16000000
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined ordered_locks.c -o ordered_locks && ./ordered_locks
All 800,000 transfers finished and the 16 accounts still held 16,000,000 between them. In 399,757 transfers the money moved to a lower-numbered account, so the thread locked the destination before the source: the order follows the numbers, never the direction of the money. Step through the two transfers of the previous lesson under the same rule:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Both start with lock 17. A moves money from 17 to 42 and B from 42 to 17. Both now lock the lower account number first, so both begin by asking for lock 17, whichever way their money moves.
- A takes lock 17. A gets lock 17 first. It is the only lock either thread may take first, so whoever holds it is the only thread that can be inside a transfer.
- B waits for lock 17. B asks for lock 17 and waits. It holds nothing while it waits, so no thread can be waiting for B.
- A takes lock 42. Lock 42 is free, because a thread takes 42 only after 17 and A holds 17. A holds both locks and makes its transfer.
- A finishes. A releases both locks. The only waiting thread waited for a lock that a running thread held, so the wait ends.
- B goes on. B takes 17 and then 42 and finishes too. With the order, a thread that waits holds only lower-numbered locks than the one it wants, so no chain of waits can come back to its start.
Real programs name the order in a few ways. Ranks give every class of lock a fixed number. A lock hierarchy: A lock order by levels: locks are grouped into numbered levels, and a thread that holds a lock of one level may take only locks of higher levels. groups locks into levels and lets a thread holding one level take only higher ones. Locks created at run time, such as one per account, are ordered by an id or an address. A lock-order checker records which lock each path takes inside which, and reports any two locks taken in both orders.
Key idea
When every thread takes its locks in one increasing order, every chain of waits climbs, so it can never close into the cycle that a deadlock needs.
Checkpoint
Check: A global lock order
Enough units leave no room for deadlock
Some resources come in identical units, such as tape drives or connections in a pool. If n processes each need at most k units, n(k − 1) + 1 units rule out deadlock: even when each process holds k − 1 and waits for its last, one unit is left, so some process finishes and returns its units. Step through the worst moment for four processes that each need up to 3 drives, with 4 × 2 + 1 = 9 drives in the pool:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Nine drives, four processes. Four processes each need up to 3 drives and ask for them one at a time. The bound gives the pool 4 × (3 − 1) + 1 = 9 drives.
- Everyone one drive short. The worst moment comes when every process holds 2 drives and waits for its third. That is the most the four can hold while all of them are short, and it takes 8 drives.
- The spare drive. One drive is still free, so it goes to the first process that asks, here P1. P1 now holds its maximum of 3.
- P1 finishes. P1 finishes and returns all 3 of its drives, so 3 are free, one for each process that is still waiting.
- Everyone finishes. P2, P3 and P4 take their third drives, finish and return them. However the requests arrive, the four can never all be one drive short with none free, so 9 drives rule out deadlock.
With every process one drive short, 8 drives were taken and the ninth let P1 finish, which freed 3 for the others. A process that needs a single unit never holds one while it waits, so it adds nothing to the count: any number of them can share one drive without deadlock.
Checkpoint
Check: The resource bound n(k − 1) + 1
Practise: the fewest units for any maximums
Write the bound for processes whose maximums differ, including processes that need one unit or none. The sample tests run in your browser.
Exercise · Medium · Python
The fewest units that rule out deadlock
Processes share the units of one resource, such as tape drives or connections, and ask for them one unit at a time. maxes[i] is the most units process i will ever hold at once; a process that reaches its maximum finishes and returns everything it holds, and a process with a maximum of 0 never asks for a unit.
Write min_units(maxes), which returns the smallest number of units with which these processes can never deadlock, whatever order their requests come in. With n identical processes of maximum k the answer is n(k − 1) + 1, so min_units([3, 3, 3, 3]) is 9; your function must also handle processes with different maximums, processes that need one unit or none, and an empty list.
Starter code · units.py
def min_units(maxes):
"""Returns the fewest units of one resource with which these processes can never deadlock.
maxes[i] is the most units process i ever holds at once; it asks for them one at a time.
"""
# Replace this line with your code.
return len(maxes) * (max(maxes, default=0) - 1) + 1 The sample tests · test_units.py
from units import min_units
def test_identical_processes():
"""four processes that each need up to 3 units: 4 x 2 + 1"""
assert min_units([3, 3, 3, 3]) == 9
def test_different_maximums():
"""processes with maximums 2, 3 and 4"""
assert min_units([2, 3, 4]) == 7
def test_one_unit_each():
"""a process that needs one unit never holds one while it waits"""
assert min_units([1, 1, 1]) == 1
def test_one_process():
"""a single process only needs its own maximum"""
assert min_units([5]) == 5
def test_a_process_that_needs_nothing():
"""a process with a maximum of 0 never asks for a unit"""
assert min_units([0, 4]) == 4
def test_no_processes():
"""with no process, no unit is needed"""
assert min_units([]) == 0 A hint
A deadlock needs every process to be stuck at the same moment: each one short of its maximum and no unit free. Ask how many units the processes can hold in total while every one of them is still short, and add one.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Practice that adapts to you
Practise this lesson
9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The resource bound, proved and checked against every allocation Pro: opens with a Pro, Premium or Ultimate pass
A pool of identical units can be made deadlock-free by owning enough of them. This part proves the bound, extends it to processes with different maximums, checks it against every possible allocation, and works through the numerical questions it produces.
In this part:
- Why the bound always suffices, and one unit fewer never does
- Different maximums: one short of every maximum, plus one
- Every allocation checked by brute force
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Lock order in real code: ranks, addresses, hierarchies and a checker Pro: opens with a Pro, Premium or Ultimate pass
A global order is easy to state and easy to break, because the second lock is often taken far from the first, in another module or a callback. This part shows how real code numbers its locks, how it orders locks created at run time, and how a checker catches a broken order before it deadlocks.
In this part:
- Ranks for fixed locks, levels for layers
- Ordering locks created at run time, by id or by address
- Checking the order across every code path
1 runnable example · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Try-lock with back-off and all-at-once requests, measured Pro: opens with a Pro, Premium or Ultimate pass
When no order can be agreed, a thread can refuse to wait while it holds a lock: it asks for every lock at once, or it gives back what it has and tries again. This part measures what each one costs, including the livelock that try-lock invites and the waiting that all-at-once requests add.
In this part:
- Try-lock and back off: the livelock it risks, measured
- What libraries do with try-lock
- Taking every lock up front: the cost in waiting, with numbers
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
The warm-up comes with its full answer:
-
Fresher to mid · Warm-up
Why does taking locks in one global order prevent deadlock?
A deadlock needs a circular wait: a closed chain of threads in which each waits for a lock that the next one holds. Give every lock a rank and let each thread take the locks it holds together only in increasing rank. A thread that holds a lock of rank i can then wait only for a lock of a higher rank, so along any chain of waiting threads the ranks keep rising. A rising chain can never come back to the thread it started from, so no cycle can form, and without a cycle there is no deadlock. The order costs nothing at run time; what it needs is that every path follows it, including paths in other modules and callbacks, which is why real code writes the ranks down and checks them.
Three harder questions follow from this lesson: transfers between accounts created at run time, the units a pool needs and a fix that burns processor time. Choose a first move for two of them:
Checkpoint
Your first decision
-
Fresher to mid
Write the locking for a transfer between two bank accounts that never deadlocks, when accounts are objects created at run time and transfers run in both directions on many threads. What do you order the locks by, and which special cases must the code handle?
-
Fresher to mid
A print server has a pool of identical printers. Six jobs each need up to 4 printers at once and ask for them one at a time. What is the fewest number of printers that guarantees no deadlock, and why does one printer fewer fail?
-
Mid to senior
A team fixed a deadlock by making each thread release its first lock and retry whenever its second lock is busy. Since then, at peak load, processor use has doubled and throughput has fallen by a third. Explain what is happening and what you would change.
Key takeaways
- One Coffman condition made impossible is enough to prevent deadlock, and each of the four costs something different.
- A global lock order costs nothing at run time: every chain of waits climbs, so it cannot close.
- n processes needing at most k units each cannot deadlock with n(k − 1) + 1 units.
Terms in this lesson
- deadlock prevention
- Designing a system so that one of the four Coffman conditions can never hold, which makes deadlock impossible whatever the timing.
- lock hierarchy
- A lock order by levels: locks are grouped into numbered levels, and a thread that holds a lock of one level may take only locks of higher levels.
- lock ordering
- Preventing deadlock by giving every lock a rank and making every thread take the locks it holds together in increasing rank, so that no chain of waits can close into a cycle.
- try-lock
- A lock call that returns at once and says whether it got the lock, instead of waiting for it; pthread_mutex_trylock and Go's Mutex.TryLock are examples.
References
- System Deadlocks (E. G. Coffman, M. Elphick and A. Shoshani), ACM Computing Surveys 3(2) (ACM)
- Hierarchical ordering of sequential processes (E. W. Dijkstra, EWD 310): the five dining philosophers (E. W. Dijkstra Archive, University of Texas at Austin)
- Runtime locking correctness validator (lockdep) (The Linux Kernel Organization)
- pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock (POSIX.1-2024) (The Open Group (The Open Group Base Specifications Issue 8))
- std::lock (cppreference.com)
- std::less (pointer total order) (cppreference.com)
- Comparison operators (C): pointer comparison (cppreference.com)
- Package sync: Mutex.TryLock (The Go Authors)
- PostgreSQL 18 documentation: explicit locking, deadlocks (The PostgreSQL Global Development Group)
- GATE 2027 syllabus for Computer Science and Information Technology (CS) (Indian Institute of Technology Madras (GATE 2027))
- GATE 2027 question paper pattern (Indian Institute of Technology Madras (GATE 2027))
Report a problem with this lesson
Kept only in this browser. Your Learn progress