Module 2 · Unit 7 · Lesson 1 of 7
Deadlock Fundamentals
The state that no process can escape: two or more processes each hold a resource the others need, and each waits forever for a resource that will never be released. Deadlock is the failure mode of synchronization — the price of the locks and semaphores of Unit 6, when they are used without a discipline. This lesson defines the problem, presents the system model of resources and requests, and shows how the cycle of waiting arises.
01 Introduction
Unit 6 presented the mechanisms that make concurrent programs correct: locks, semaphores, monitors, and condition variables. Each of them is a tool for ensuring that shared state is accessed consistently and that processes wait for conditions to become true. Used correctly, those tools produce programs that are correct. Used incorrectly — or used correctly but in an unlucky interleaving — they can produce a state in which no process can make progress. That state is deadlock.
Deadlock is the situation in which a set of processes is permanently blocked because each is waiting for a resource that another process in the set holds. Because every process in the set is waiting, none of them can release the resources they hold, and the wait is permanent. The system does not crash; it simply stops making progress. On a general-purpose machine, a deadlock in a user program may leave a few threads hung while the rest of the system runs normally. In a kernel, a deadlock can hang the entire machine. In a database, a deadlock can prevent a transaction from committing. The consequences range from annoying to catastrophic, and the problem is one of the classical concerns of operating-system design.
The study of deadlock has a long history. Edsger Dijkstra identified the problem in the 1960s in the context of the dining philosophers problem, and the deadly embrace of two tape drives was a familiar failure mode in the batch systems of the era. The formal treatment — the four necessary conditions, the resource allocation graph, the Banker's algorithm — was developed during the 1970s and remains the standard pedagogical framework. This unit presents that framework in the six lessons that follow.
This first lesson sets the foundation. It defines deadlock and distinguishes it from the related phenomena of starvation and livelock. It presents the system model of resources and requests, describes the lifecycle of a resource from request to release, and shows how the cycle of waiting arises. It examines the classic real-world examples — the dining philosophers, the bridge crossing, the traffic intersection — and shows how the same pattern appears in concurrent programs. It closes with a discussion of the scope of the problem and the four strategies that operating systems use to deal with it: prevention, avoidance, detection, and recovery.
Prerequisites
This lesson builds on Unit 6. Familiarity with the lock (Lesson 6.3), the semaphore (Lesson 6.4), and the condition variable (Lesson 6.6) is assumed. The discussion of lock-ordering deadlock in section 4.9 of Lesson 6.3 is the direct motivation for this unit; if that section is unfamiliar, review it before continuing. No prior knowledge of deadlock theory is assumed.
02 Learning objectives
By the end of this lesson you will be able to
- Define deadlock and describe the system model of resources, requests, and allocations.
- Trace the resource lifecycle — request, use, release — and identify the point at which a process may block.
- Explain how a circular wait arises and why it makes progress impossible.
- Distinguish deadlock from starvation and livelock.
- Describe the four strategies for dealing with deadlock: prevention, avoidance, detection, and recovery.
This lesson contributes to U7.1 and U7.2, and to course learning outcomes CLO2, CLO3, and CLO8.
03 Required reading
- Operating System Concepts — Chapter 8, Sections 8.1–8.3, System Model, Deadlock in Multithreaded Applications, and Deadlock Characterization. The standard treatment of the system model and the four necessary conditions.
- OSTEP — Chapter 32, Concurrency Bugs, through the section on Deadlock. OSTEP's treatment focuses on the practical programming view: deadlock as a bug in a concurrent program, introduced through the code that produces it.
- Tanenbaum & Bos — Modern Operating Systems, Chapter 6, Deadlocks, through the section on deadlock detection. A comprehensive treatment that presents the four conditions, the resource-allocation graph, and the dining philosophers alongside the theory.
- Silberschatz et al. — Chapter 8, Sections 8.4–8.8. The formal treatment of the resource allocation graph and the four strategies for handling deadlock.
- Dijkstra, E. W. — Hierarchical Ordering of Sequential Processes. Acta Informatica, 1971. The paper that introduced the dining philosophers problem and the concept of a deadlock.
-
Linux man pages:
man 7 pthreads(the section on deadlock),man 3 pthread_mutex_init(the discussion of the error-checking and robust mutex types),man 2 fcntl(the record-locking interface, which can deadlock).
Reading strategy
Read the Silberschatz section on the system model first; it establishes the vocabulary — resources, requests, allocations — that the rest of the unit depends on. Then read OSTEP's section on deadlock for the programming perspective, which shows the same phenomenon from the code side. The two views complement each other: the textbook treats deadlock as a property of a system of processes and resources, and OSTEP treats it as a bug that appears in a concurrent program with locks. Both are correct, and the same theory underlies both.
04 Core content
4.1 What is deadlock?
A deadlock is a situation in which a set of two or more processes is permanently blocked because each process in the set is waiting for a resource that another process in the set holds. Because every process is waiting, none of them can release the resource it holds, and none of them can acquire the resource it is waiting for. The processes are stuck forever.
The defining feature of deadlock is mutual, permanent blockage. It is not the same as one process waiting for a resource that another process will eventually release; in that case, progress is slow but does happen. Deadlock is the case where no process in the set can proceed, no matter how long the system runs, because the conditions that would allow progress cannot be satisfied without external intervention.
The simplest illustration is two processes and two resources. Process P1 holds resource R1 and needs R2 to continue. Process P2 holds R2 and needs R1. Each waits for a resource the other holds. Neither will release its resource until it gets what it is waiting for. The system cannot make progress.
4.2 The system model
To reason about deadlock precisely, we need a model of the system. The standard model has three elements: resources, processes, and the requests that processes make for resources.
- Resources
- A resource is any entity that a process may need to complete its work. Resources come in types, each with some number of instances. Examples: a printer (type: printer, one instance), a memory page (type: page frame, many instances), a file (type: file, one instance per open), a semaphore (type: semaphore, one instance), a lock (type: mutex, one instance), a CPU core (type: core, one instance per core).
- Processes
- A process (or thread, or task) is an execution context that uses resources to make progress. Each process follows a sequence of operations: it requests resources, uses them, and releases them.
- Requests
- A request is an operation by which a process asks for an instance of a resource type. If an instance is available, the request is granted; if none is available, the process may block until one becomes available.
The model abstracts away the physical details. A mutex is a resource with one instance. A counting semaphore is a resource with N instances, where N is the initial value. A file is a resource with one instance per open. A printer is a resource with one instance. The model does not care about the physical form; it cares only about the operations that processes perform on the resource and the way the resource is shared among them.
A resource is reusable if it can be released and reacquired by another process. A lock is reusable: a process releases it and another process acquires it. A message is not reusable: once consumed, it is gone. Deadlock in the classical sense concerns reusable resources, because the cycle of waiting depends on the fact that a process can hold a resource for a long time. A slightly different phenomenon, called resource starvation, arises with consumable resources, and is covered in Lesson 7.2.
4.3 The resource lifecycle
Every use of a resource by a process follows the same three-step pattern:
- Request. The process asks the operating system for an instance of the resource. If the request cannot be granted immediately — because all instances are in use — the process is placed on a wait queue for that resource and blocks.
- Use. The process uses the resource. For a lock, this is the critical section; for a file, this is the read or write; for a printer, this is the print job.
- Release. The process releases the resource. The operating system may then grant a pending request from another process that is waiting for the resource.
A process may request many resources over its lifetime, and at any moment it may hold several at once. The pattern of requests and releases is called the process's resource usage sequence. A process may request a resource, use it, release it, then request a different resource, and so on. The set of resources a process holds at any moment is its holding set; the set of resources it is waiting for is its waiting set.
The deadlock condition arises when the holding set of one process intersects the waiting set of another, and the waiting set of the first intersects the holding set of the second. The cycle of mutual dependency is the defining feature of deadlock.
4.4 How the cycle arises
The cycle of waits and holdings does not arise from a single unfortunate request. It arises from the interaction of several requests over time. The following trace illustrates how the simple two-process deadlock of Figure 7.1 develops.
| Step | Process P1 | Process P2 | State |
|---|---|---|---|
| 0 | idle | idle | both resources free |
| 1 | request R1 → granted | idle | P1 holds R1 |
| 2 | holds R1 | request R2 → granted | P1 holds R1, P2 holds R2 |
| 3 | request R2 → blocked (held by P2) | holds R2 | P1 holds R1, waits for R2 |
| 4 | blocked | request R1 → blocked (held by P1) | P2 holds R2, waits for R1 |
| 5 | blocked forever | blocked forever | deadlock |
At each step before the deadlock, the operating system made a reasonable decision. At step 1, R1 was free, so it was granted to P1. At step 2, R2 was free, so it was granted to P2. The deadlock was not caused by a mistake at any one step; it was caused by the way the requests interleaved. If P1 had requested R2 before P2 requested it, or if P2 had not requested R1 until after P1 had released R2, no deadlock would have occurred.
Deadlock is a race condition
Like the race conditions of Lesson 6.1, deadlock depends on the interleaving chosen by the scheduler. The same program with the same input may deadlock on one run and complete normally on another. The bug is not in any single step but in the interaction between steps. This is why deadlock is so difficult to test for: a program that does not deadlock on a given run has not been proven deadlock-free.
4.5 Real-world examples
Deadlock is not a theoretical construct. It appears in operating-system kernels, database systems, embedded systems, network protocols, and any concurrent program that uses multiple locks. The following examples are the classics.
- Dining philosophers. Five philosophers sit at a round table, each alternating between thinking and eating. Between each pair of adjacent philosophers lies a single fork — five forks for five philosophers. To eat, a philosopher must pick up both forks adjacent to them. If every philosopher picks up their left fork at the same time, all five wait forever for their right fork, which their neighbour holds. This is the canonical illustration of deadlock, introduced by Dijkstra in 1971.
- The deadly embrace. The classic early-computing example: two processes each need two tape drives; process A holds drive 1 and requests drive 2, while process B holds drive 2 and requests drive 1. Neither can proceed, and the system hangs. The term deadly embrace is a historical name for deadlock.
- Bridge crossing. Two cars approach a narrow bridge from opposite ends. The bridge is only wide enough for one car at a time. Car A enters the bridge and stops because car B has also entered from the other end. Neither can move forward and neither can back up; both are stuck. This is a deadlock of a spatial resource.
- Traffic intersection. Four cars arrive at a four-way intersection simultaneously, each going straight. If the intersection has no traffic lights, and each car enters the intersection and waits to see what the others will do, all four block each other. This is a deadlock of a shared spatial resource.
- Database transaction deadlock. Two transactions each acquire a lock on one row of a table and then try to acquire a lock on the other's row. Neither can proceed; the database system must detect the deadlock and abort one of the transactions. This is a common occurrence in a busy database, and modern database systems have sophisticated mechanisms for handling it.
- Kernel deadlock. The Linux kernel uses many locks, and a bug in the locking discipline can deadlock the entire machine. The kernel's spinlock and mutex primitives are designed to detect and warn about some forms of misuse — for example, acquiring a mutex twice from the same thread — but not every form of deadlock is detectable. Kernel deadlock is one of the reasons the discipline of consistent lock ordering is so important in kernel development.
- Priority inversion. A related problem, examined in Lesson 5.4, in which a high-priority task is delayed by a low-priority task holding a resource. This is not deadlock in the strict sense — the low-priority task can make progress — but it produces a similar effect of a high-priority task being unable to run. The Mars Pathfinder incident is the canonical example.
4.6 Deadlock, starvation, and livelock
Three related phenomena are often confused. They are distinct, and the distinctions matter for diagnosis and solution.
| Phenomenon | Definition | Can the system recover without intervention? |
|---|---|---|
| Deadlock | A set of processes is permanently blocked because each waits for a resource another holds. No process can proceed. | No. The processes are blocked and cannot act. |
| Starvation | A process is indefinitely postponed because other processes keep taking the resource it wants. The system as a whole makes progress, but this one process does not. | Yes, in principle — if the other processes eventually stop or if an aging policy is in place. |
| Livelock | Processes are not blocked, but they repeatedly change state in response to each other without making progress. They are busy but not useful. | In principle, yes — if the state changes stop. In practice, often not without intervention. |
The distinction between deadlock and starvation is the most important. A deadlocked process is blocked; a starved process is runnable but never scheduled, or is repeatedly overtaken by other processes. A deadlocked system makes no progress at all; a system with starvation makes progress overall, but one specific process does not. The solution to starvation is aging (Lesson 5.4); the solution to deadlock is prevention, avoidance, or detection and recovery (the rest of this unit).
Livelock is the more subtle case. A livelocked process is not blocked — it is running — but it is not making progress. The classic example is two people walking toward each other in a narrow corridor: each steps to the side to let the other pass, they step the same direction, then both step the other way, and so on. In a concurrent program, livelock appears in lock-free algorithms where two threads repeatedly retry a compare-and-swap operation, each invaliding the other's attempt, so neither makes progress. Livelock is rarer than deadlock and harder to detect, because the system appears busy.
A helpful visualisation
Imagine two trains on a single track, approaching each other from opposite ends. If neither can back up, they are deadlocked. If both can back up but keep moving forward in an endless dance, they are livelocked. If one train can proceed but the other is repeatedly delayed because the first never stops, the second is starved. The three phenomena look similar on the surface but have different causes and different solutions.
4.7 Deadlock in concurrent programs
In the previous unit, we saw that locks and semaphores are the tools for making concurrent programs correct. The price of those tools is the possibility of deadlock. A program that acquires two locks in inconsistent order across different threads is a program that can deadlock.
The pattern is direct: if thread T1 acquires lock L1 and then lock L2, while thread T2 acquires L2 and then L1, and both reach their second acquisition before either reaches its release, then T1 holds L1 and waits for L2, and T2 holds L2 and waits for L1. The deadlock is not caused by a bug in either thread; both threads are doing what they were written to do. The deadlock is caused by the interaction between them.
The standard prevention technique is a global lock order. All threads acquire locks in the same order — always L1 before L2, never the reverse. If every thread follows the same order, no cycle of waits and holdings can form, because the cycle would require a thread to wait for a lock with a lower order than one it already holds, which the discipline forbids. This is the simplest and most widely used form of deadlock prevention, and it is the subject of Lesson 7.4.
Lock ordering is easy to state and hard to maintain. A large program has many locks and many code paths, and the discipline must be applied consistently everywhere. A single function that acquires locks in the wrong order, or a single code path that violates the order under some condition, can reintroduce the possibility of deadlock. This is why some systems avoid the problem entirely by using a single global lock, at the cost of serializing all concurrent work — a trade-off examined in Lesson 6.3.
4.8 The four strategies
Operating systems have four strategies for dealing with deadlock. Each is appropriate in a different context.
- Prevention
- Design the system so that at least one of the necessary conditions for deadlock can never hold. The usual approach is to impose a global lock order (breaking the circular-wait condition) or to require that a process acquire all its resources at once (breaking the hold-and-wait condition). Prevention guarantees that deadlock cannot occur, at the cost of restricting the system's behaviour. Examined in Lesson 7.4.
- Avoidance
- Allow the necessary conditions to hold, but make scheduling decisions that keep the system in a safe state — a state from which every process can still complete. Before granting a resource request, the system checks whether granting it would leave the system in an unsafe state; if so, the request is deferred. Avoidance is more flexible than prevention but requires advance knowledge of each process's maximum resource needs. Examined in Lessons 7.5 and 7.6.
- Detection and recovery
- Allow deadlock to occur, detect it when it does, and recover by aborting one or more processes or rolling them back. Detection is done periodically by examining the state of the system for a cycle of waits and holdings. Recovery requires a policy for choosing which process to abort and how to undo its effects. Examined in Lesson 7.7.
- Ignorance
- Ignore the problem and hope it does not occur. If deadlocks are rare enough — a few per year on a system that runs millions of operations — the cost of preventing or detecting them may exceed the cost of simply rebooting when one occurs. This is the strategy used by most general-purpose operating systems, including Linux, for user-level deadlocks. It is called the ostrich algorithm: bury your head in the sand and hope the problem goes away.
The four strategies are not mutually exclusive. A system may prevent some forms of deadlock, avoid others, detect and recover from still others, and ignore the rest. Linux, for example, uses lock ordering in the kernel to prevent kernel deadlocks, provides error-checking and robust mutex types to help user programs detect some forms of misuse, and relies on the ostrich algorithm for user-space deadlocks — a user program that deadlocks stays hung until it is killed. The choice of strategy depends on the cost of the mechanisms and the frequency of the problem.
4.9 Common misconceptions
"Deadlock and starvation are the same."
They are different. A deadlocked process is blocked and cannot proceed at all; a starved process is runnable but never chosen. The system makes progress under starvation; it does not under deadlock.
"Deadlock is a bug in one process."
Deadlock is a property of the interaction between processes. Each process may be individually correct, and the deadlock arises from the way their requests interleave.
"Deadlock and livelock are the same."
A deadlocked process is blocked. A livelocked process is running but not making progress. Livelock is busier and harder to detect; deadlock is quieter.
"Deadlock only happens with locks."
Deadlock can occur with any resource that processes hold and wait for. Semaphores, files, memory, tape drives, printers, and even physical space (a bridge) can all be the source of deadlock.
"Adding a timeout fixes deadlock."
A timeout converts deadlock into starvation, or into a livelock, or into incorrect behaviour. It does not address the underlying cycle of waits and holdings.
"Deadlock is rare, so we can ignore it."
Deadlock frequency depends on the workload. On a busy database it can occur many times per hour; in a kernel it can hang the machine; on an idle desktop it may never occur. Frequency determines the right strategy.
"A single lock prevents deadlock."
Yes, in the sense that a single lock cannot form a cycle of waits and holdings. But a single lock serializes the program, which destroys the benefit of concurrency. The trade-off is real.
"Deadlock means the CPU is idle."
The CPU may be idle, or it may be busy running other processes. Deadlock concerns the processes in the deadlock cycle; the system as a whole may be busy with unrelated work.
05 Worked example
The following example traces a two-thread lock-ordering deadlock in C, shows the interleaving that produces it, and demonstrates the fix.
Two threads, two locks, one deadlock
Two threads each acquire two locks, in opposite orders:
pthread_mutex_t L1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t L2 = PTHREAD_MUTEX_INITIALIZER;
void *t1(void *arg) {
pthread_mutex_lock(&L1);
sleep(1); /* make the race easier to observe */
pthread_mutex_lock(&L2);
/* critical section */
pthread_mutex_unlock(&L2);
pthread_mutex_unlock(&L1);
return NULL;
}
void *t2(void *arg) {
pthread_mutex_lock(&L2);
sleep(1);
pthread_mutex_lock(&L1);
/* critical section */
pthread_mutex_unlock(&L1);
pthread_mutex_unlock(&L2);
return NULL;
}
If both threads run concurrently, the following interleaving produces a deadlock.
| Time | Thread T1 | Thread T2 | L1 held by | L2 held by |
|---|---|---|---|---|
| 0 | lock(L1) → granted | — | T1 | — |
| 1 | sleep(1) | lock(L2) → granted | T1 | T2 |
| 2 | sleeping | sleep(1) | T1 | T2 |
| 3 | lock(L2) → blocked | sleeping | T1 | T2 |
| 4 | blocked on L2 | lock(L1) → blocked | T1 | T2 |
| 5 | blocked forever | blocked forever | T1 | T2 |
At time 3, T1 requests L2, which is held by T2, and
blocks. At time 4, T2 requests L1, which is held by T1,
and blocks. Both threads are now blocked waiting for a
lock the other holds. Neither will release its lock,
because neither can proceed past the second
lock call. The program is deadlocked.
Demonstrating the deadlock
Run the program from a terminal. It appears to hang.
From a second terminal, use gdb to attach to
the running process and inspect the state of each
thread:
gdb -p $(pgrep deadlock)
(gdb) thread apply all bt
(gdb) info threads
(gdb) quit
The backtraces show both threads blocked inside
pthread_mutex_lock, one on L2 and one on
L1. Neither is making progress. The process will remain
in this state until it is killed with kill
-9 or the machine is restarted.
The fix: consistent lock ordering
The standard fix is to acquire the locks in a consistent order. If both threads always acquire L1 before L2, no deadlock can occur.
void *t2_fixed(void *arg) {
pthread_mutex_lock(&L1); /* acquire in the same order as T1 */
sleep(1);
pthread_mutex_lock(&L2);
/* critical section */
pthread_mutex_unlock(&L2);
pthread_mutex_unlock(&L1);
return NULL;
}
With this fix, the interleaving that produced the deadlock is no longer possible. If T1 has L1 and T2 requests L1, T2 blocks until T1 releases L1 — but T1 can proceed, because the only lock it needs is L2, which T2 has not acquired (T2 is blocked on L1). T1 acquires L2, finishes, and releases both locks. T2 then acquires L1 and L2 in turn. The program runs to completion.
Alternative fix: trylock with backoff
A different fix is to use pthread_mutex_trylock
instead of pthread_mutex_lock for the
second lock. If the second lock cannot be acquired
immediately, the thread releases the first lock and
retries the whole sequence.
void *t1_retry(void *arg) {
for (;;) {
pthread_mutex_lock(&L1);
if (pthread_mutex_trylock(&L2) == 0) {
/* got both locks */
pthread_mutex_unlock(&L2);
pthread_mutex_unlock(&L1);
return NULL;
}
/* could not get L2; release L1 and retry */
pthread_mutex_unlock(&L1);
sched_yield(); /* give the other thread a chance */
}
}
This is the acquire-and-back-off approach. It
avoids deadlock because a thread that cannot acquire
both locks never holds one while waiting for the other.
It introduces the possibility of livelock — both threads
could repeatedly acquire their first lock, fail to
acquire the second, release, and retry — which is why
the sched_yield is included. Lock ordering
is preferable in most cases; retry is useful when the
locks do not have a natural order.
Comparing to the dining philosophers
The dining philosophers problem has the same structure. Each philosopher is a process, each fork is a lock, and the deadlock arises when each philosopher picks up their left fork and waits for their right. The fix by lock ordering is to number the forks and require each philosopher to pick up the lower-numbered fork first. A philosopher who needs fork i and fork (i+1) mod 5 always picks up the lower-numbered first. The philosopher with the highest-numbered fork and the lowest-numbered fork swaps the order of acquisition. This simple discipline prevents the deadlock.
The lock-ordering discipline generalizes: assign each resource a total order, and require every process to acquire its resources in that order. The next lesson formalizes the condition that makes this work — the absence of a circular wait — and presents it as one of the four necessary conditions for deadlock.
Measuring the deadlock
A simple way to observe the deadlock without gdb
is to use the /proc file system. A
deadlocked process is sleeping in the kernel waiting
for a lock; its state will be S (sleeping)
and its wchan will point to a lock-related function.
PID=$(pgrep deadlock)
cat /proc/$PID/status | grep -E 'State|Threads'
cat /proc/$PID/task/*/stat | awk '{print $1, $2, $3}'
Each thread's state and waiting channel is listed. On a
Linux system with the appropriate debugging support, the
kernel reports that the threads are waiting on
futex_wait. This confirms the deadlock and
points to the specific locks involved.
06 Interactive activity
Activity · reproduce and observe deadlock
The goal of this activity is to write a deadlocking program, observe the deadlock from outside, and confirm that the fix eliminates it. Work through the steps in your course VM.
-
Write a deadlocking program.
Write the C program from the worked example with two
threads and two locks acquired in opposite orders.
Compile with
gcc -O2 -pthread -o deadlock deadlock.cand run it. The program should hang.
If the program does not hang, run it several times. The deadlock depends on the interleaving chosen by the scheduler; on a heavily loaded system it will occur more often../deadlock ^C # interrupt with Ctrl-C to kill it -
Inspect the hung process with gdb.
Run the program again and, from a second terminal,
attach
gdbto it. Useinfo threadsto list the threads, thenthread apply all btto see the backtrace of each. Identify which thread is blocked on which lock.
The backtraces should show both threads blocked insidegdb -p $(pgrep deadlock) (gdb) info threads (gdb) thread apply all bt (gdb) quitpthread_mutex_lock. -
Inspect the process through /proc.
With the program still hung, use
/proc/<pid>/task/to inspect the threads.
On a system with the necessary debugging support, the kernel reports the threads waiting on a futex. This confirms that the threads are blocked in the kernel, not spinning in user space.PID=$(pgrep deadlock) ls /proc/$PID/task/ cat /proc/$PID/status | grep -E 'State|Threads' cat /proc/$PID/stack # may require root -
Apply the lock-ordering fix.
Modify the program so that both threads acquire the
locks in the same order. Rebuild and run. The program
should now complete normally. Run it many times — a
hundred, say — and confirm that no deadlock occurs.
This does not prove that the program is deadlock-free in every possible interleaving, but it is good evidence that the change has eliminated the common case.for i in $(seq 1 100); do ./deadlock_fixed || echo "failure on run $i" done -
Try the trylock-with-retry fix.
Modify the program to use
pthread_mutex_trylockwith a backoff, as shown in the worked example. Run the program. Does it complete? Does it use more CPU than the lock-ordering version? Usetimeto compare the two versions. Which one is faster for this workload?time ./deadlock_retry time ./deadlock_fixed -
Reproduce the dining philosophers deadlock.
Write a small simulation of the dining philosophers
problem with five threads and five locks (one per
fork). Each philosopher acquires the fork to their
left, then the fork to their right, then eats, then
releases both. Run the program. It should deadlock
almost immediately — all five philosophers pick up
their left fork and wait for their right fork.
Then apply the lock-ordering fix (each philosopher acquires the lower-numbered fork first) and confirm that the deadlock disappears.pthread_mutex_t forks[5]; void *philosopher(void *arg) { long i = (long)arg; pthread_mutex_lock(&forks[i]); /* left */ pthread_mutex_lock(&forks[(i + 1) % 5]); /* right */ /* eat */ pthread_mutex_unlock(&forks[(i + 1) % 5]); pthread_mutex_unlock(&forks[i]); return NULL; } -
Observe the CPU usage of a deadlocked program.
While a deadlocked program is hung, use
toporpsto observe its CPU usage. A deadlocked program uses zero CPU, because all its threads are blocked in the kernel. Compare this to a livelocked program — if you have written one — which uses a lot of CPU because its threads are running. This is a useful diagnostic: a hung program using zero CPU is likely deadlocked; a hung program using lots of CPU is likely livelocked.ps -o pid,stat,pcpu,comm -p $(pgrep deadlock) -
Read the pthreads man page on deadlock.
Read
man 7 pthreads, in particular the section on "Threads and Mutexes" and the section on deadlock. What does the man page say about the conditions under which deadlock occurs? What advice does it give for avoiding it?
Reflect: the deadlock in this activity is entirely deterministic once the interleaving is fixed: T1 always acquires L1 first, T2 always acquires L2 first, and the cycle of waits and holdings is inevitable. But in a more realistic program, the interleaving depends on the scheduler, and the deadlock may occur only one run in a thousand. How would you test for such a bug? What tools and techniques would you use? What if a bug in production occurred only once a month, and you could never reproduce it in a test environment?
07 Knowledge check
Check your understanding
Question 1
Define deadlock. Describe the three elements of the system model — resources, processes, and requests — and give an example of each from a modern operating system.
Show answer
Question 2
Describe the resource lifecycle: request, use, release. At which step does a process typically block, and why? What is the relationship between a process's holding set and its waiting set, and how does this relationship lead to deadlock?
Show answer
Question 3
Explain the difference between deadlock, starvation, and livelock. Give an example of each, and describe how you would distinguish them in a running program.
Show answer
S (sleeping) or D
(uninterruptible sleep). A starved program uses
CPU, and its starved thread is runnable but never
scheduled; the other threads in the program make
progress. A livelocked program uses a great deal of
CPU, and its threads are continuously running but
never finishing. The CPU usage pattern is often the
fastest diagnostic.
Question 4
Describe the four strategies for dealing with deadlock: prevention, avoidance, detection and recovery, and ignorance. Under what circumstances is each appropriate? Why does Linux use the ostrich algorithm for user-space deadlocks?
Show answer
- Prevention. Design the system so that at least one of the necessary conditions for deadlock can never hold. The usual approach is to impose a global lock order or to require that a process acquire all its resources at once. Prevention guarantees that deadlock cannot occur, at the cost of restricting the system's behaviour. Appropriate when deadlock is catastrophic (a kernel hang) or frequent (a busy database).
- Avoidance. Allow the necessary conditions to hold, but make scheduling decisions that keep the system in a safe state. Before granting a resource request, the system checks whether granting it would leave the system in an unsafe state; if so, the request is deferred. Avoidance is more flexible than prevention but requires advance knowledge of each process's maximum resource needs — often unavailable. Appropriate when the resource needs are known in advance (an embedded system with a fixed task set).
- Detection and recovery. Allow deadlock to occur, detect it when it does, and recover by aborting or rolling back one or more processes. Appropriate when the cost of detection is low and deadlocks are rare — a database system that aborts one transaction is a common example.
- Ignorance. Ignore the problem and hope it does not occur. Appropriate when deadlocks are rare enough that the cost of preventing or detecting them exceeds the cost of the occasional hang. This is the ostrich algorithm.
Question 5
Two threads each acquire two locks in opposite order and deadlock. Show the trace of requests and allocations that produces the deadlock, and describe two different ways to fix the program.
Show answer
- T1: lock(L1) → granted. T1 holds L1.
- T2: lock(L2) → granted. T2 holds L2.
- T1: lock(L2) → blocked, because L2 is held by T2.
- T2: lock(L1) → blocked, because L1 is held by T1.
- Deadlock. Neither thread can proceed; neither will release its lock, because both are blocked inside the second lock call.
- Consistent lock ordering. Require both threads to acquire the locks in the same order. If both acquire L1 before L2, no deadlock can occur, because a thread holding L1 and waiting for L2 does not block any thread waiting for L1 — the other thread cannot acquire L1 until the first thread releases it, and the first thread can always eventually release it because it can acquire L2. This is the standard and preferred fix.
- Trylock with backoff. Use
pthread_mutex_trylockfor the second lock. If the second lock cannot be acquired immediately, release the first lock and retry the whole sequence. A thread that cannot acquire both locks never holds one while waiting for the other, so the cycle of waits and holdings cannot form. This fix can lead to livelock if both threads repeatedly fail in lockstep, so a backoff (such assched_yieldor a small sleep) is usually included. It is useful when the locks have no natural order.
08 Unit discussion
Discussion prompt
The ostrich algorithm — ignoring the problem and hoping it does not occur — is used by Linux and most other general-purpose operating systems for user-space deadlocks. The reasoning is that deadlocks are rare enough that the cost of preventing or detecting them exceeds the cost of the occasional hang. This is a reasonable engineering decision in the general case, but it means that a user program that deadlocks simply hangs, and the user must kill it manually.
Is this the right trade-off? Some systems do more. Database systems detect and recover from deadlock automatically, aborting one transaction so the other can proceed. Real-time systems prevent deadlock by design, using the priority ceiling protocol or the highest-locker protocol. Some languages (Rust) prevent many deadlock scenarios at compile time through ownership rules, though not all of them. Others (Java, C#) provide tools that detect lock-order inconsistencies at runtime.
Should a general-purpose operating system do more to help user programs avoid or recover from deadlock? Could it provide a lock-ordering framework that programs opt into? Could it detect cyclic waits in user-space locks and abort a thread? What would be the cost, and is it worth paying? Consider the design of an embedded system, a server, a mobile operating system, and a desktop operating system — would your answer be the same for all of them?
Post your response to the Unit 7 discussion forum and reply to at least two classmates. Supports CLO2, CLO3, and CLO8.
09 Practice exercise
This ungraded exercise consolidates the lesson and prepares you for Lesson 7.2. Aim to spend 60–90 minutes on it.
- Define deadlock in your own words. Write a definition of deadlock in two sentences. Then distinguish deadlock from starvation in two more sentences, giving one example of each. Finally, distinguish deadlock from livelock in two more sentences, again with an example of each.
- Trace a two-process deadlock. Two processes, P1 and P2, share two resources, R1 and R2, each with one instance. Show a trace of requests and releases that produces a deadlock. Then show a different trace with the same requests in a different order that does not produce a deadlock. Explain what makes the difference.
-
Write a deadlocking program.
Write the C program from the worked example. Compile
and run it. Confirm that it hangs. Use
gdbto attach and inspect the state of the two threads. What does each thread's backtrace show? Which lock is each thread blocked on? -
Fix the program.
Modify the program to acquire the locks in a
consistent order. Rebuild and run it 100 times.
Confirm that no deadlock occurs. Then modify it again
to use
pthread_mutex_trylockwith backoff. Which version is faster? Which is simpler to reason about? Which would you use in production, and why? -
Simulate the dining philosophers.
Write a program with five philosopher threads and five
fork locks. Have each philosopher acquire their left
fork and then their right fork, eat, then release both.
Run the program and confirm that it deadlocks. Then
apply the lock-ordering fix (each philosopher acquires
the lower-numbered fork first) and confirm that the
deadlock disappears.
pthread_mutex_t forks[5]; void *philosopher(void *arg) { long i = (long)arg; int first = (i < (i + 1) % 5) ? i : (i + 1) % 5; int second = (i < (i + 1) % 5) ? (i + 1) % 5 : i; pthread_mutex_lock(&forks[first]); pthread_mutex_lock(&forks[second]); /* eat */ pthread_mutex_unlock(&forks[second]); pthread_mutex_unlock(&forks[first]); return NULL; } -
Observe the difference between deadlock and livelock.
Write two small programs. The first is the deadlocking
program from question 3. The second is a simple
spin-wait loop that runs forever without making
progress — for example, a while loop that tests a
condition that is never satisfied. Run both and use
ps -o pid,stat,pcpu,commto compare their CPU usage and state. What does the difference tell you about how to distinguish the two phenomena? - Design a lock-order policy. For a small system with five locks — A, B, C, D, E — design a lock-order policy. Which lock is acquired first? Which second? Give a table listing the order for a program that uses all five. Then consider what happens when a program path acquires D before B — would this violate the policy, and would it necessarily deadlock? (Hint: not every violation of the order causes a deadlock; the point of the policy is to make it impossible for a cycle to form.)
- Reflect. In your own words, explain why a program that deadlocks is not necessarily a program with a bug in any single process. Then describe a real-world situation — not in a computer — where deadlock can arise. What are the resources, the processes, and the cycle of waits and holdings in your example?
10 Summary
- A deadlock is a situation in which a set of processes is permanently blocked because each waits for a resource that another holds. Because every process is waiting, none can release its resource, and the system makes no progress.
- The system model has three elements: resources (entities a process needs, in types with instances), processes (execution contexts that use resources), and requests (operations by which a process asks for a resource).
- The resource lifecycle has three steps: request (the process may block if no instance is available), use, and release. A process that blocks in the request step has added the resource to its waiting set.
- A deadlock arises when the waits and holdings form a cycle: the holding set of one process intersects the waiting set of another, and vice versa. The cycle has no exit, so no process can proceed.
- Deadlock is a race condition: the same program with the same input may deadlock on one run and complete normally on another, depending on the interleaving chosen by the scheduler.
- Classic examples include the dining philosophers, the deadly embrace of two tape drives, the bridge crossing, the traffic intersection, and the database transaction deadlock. The same pattern appears in any concurrent program that uses multiple locks.
- Deadlock, starvation, and livelock are three distinct phenomena. In deadlock, processes are blocked and the system makes no progress. In starvation, one process is indefinitely postponed while the system makes progress. In livelock, processes are running but not making progress.
- The standard prevention technique for deadlock in concurrent programs is global lock order: all threads acquire locks in the same order, so that no cycle of waits and holdings can form.
- Operating systems use four strategies to deal with deadlock: prevention (make it impossible), avoidance (steer clear of unsafe states), detection and recovery (detect and break the cycle), and ignorance (the ostrich algorithm).
- Linux uses the ostrich algorithm for user-space deadlocks, on the grounds that they are rare and the cost of preventing them would be imposed on all programs.
Looking ahead
This lesson defined deadlock and presented the system model. Lesson 7.2 presents the four necessary conditions for deadlock — mutual exclusion, hold-and-wait, no preemption, and circular wait — and shows how each condition can be attacked by a prevention strategy. Lesson 7.3 introduces the resource-allocation graph, the standard tool for analysing the state of a system of processes and resources and detecting the presence of a cycle. Lessons 7.4–7.6 present prevention, avoidance, and the Banker's algorithm. Lesson 7.7 closes the unit with detection and recovery, and with the classic problems that appear throughout the deadlock literature.
11 References
- Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 8, Sections 8.1–8.3.
-
Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C.
Operating Systems: Three Easy Pieces, Chapter 32.
Available online at
ostep.org. - Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 6.
- Dijkstra, E. W. Hierarchical Ordering of Sequential Processes. Acta Informatica, 1971. The paper that introduced the dining philosophers problem.
- Coffman, E. G., Elphick, M. J., & Shoshani, A. System Deadlocks. ACM Computing Surveys, 1971. The paper that formalized the four necessary conditions for deadlock.
- Holt, R. C. Some Deadlock Properties of Computer Systems. ACM Computing Surveys, 1972. The paper that introduced the resource-allocation graph.
- Havender, J. W. Avoiding Deadlock in Multitasking Systems. IBM Systems Journal, 1968. The first published treatment of lock ordering.
- Love, R. Linux Kernel Development, 3rd ed., Chapter 9, Kernel Synchronization Methods. The discussion of deadlock in the kernel.
-
Linux man-pages:
man 7 pthreads,man 3 pthread_mutex_init,man 3 pthread_mutex_lock,man 2 fcntl.