Deadlock
Basic Concepts
Definition of Deadlock
Deadlock: a situation in which several processes wait for one another because they compete for resources; without outside intervention, none of them can make progress.
Characteristics of deadlock:
- Mutual exclusion: a resource cannot be used by several processes at the same time
- Hold and wait: a process does not release the resources it holds while waiting for other resources
- No preemption: a resource cannot be forcibly taken away
- Circular wait: a chain of waiting processes forms a cycle
A Deadlock Example
A classic deadlock scenario:
Process A holds resource R1 and requests resource R2
Process B holds resource R2 and requests resource R1
Resource allocation graph:
R1 -----> P1
| |
| v
| P2
| |
R2 <------|
Deadlock Prevention
Breaking Mutual Exclusion
Strategy: use shareable resources so that mutual exclusion is never needed.
Methods:
- Use read-only resources
- Use virtualization
- Use resource pools
Example:
// Use shared memory instead of a mutex
shared_memory *data = get_shared_memory();
// Several processes can read at the same time
read_data(data);
Breaking Hold and Wait
Strategy: a process must request all the resources it needs at once.
Methods:
- Resource pre-allocation: the process requests all resources before it starts running
- Resource release: the process releases every resource it holds before requesting a new one
Example:
// Request all resources at once
void process() {
acquire_all_resources();
// do the work
release_all_resources();
}
Breaking No Preemption
Strategy: allow resources to be forcibly taken away.
Methods:
- Assign priorities to resources
- Implement a resource preemption mechanism
- Use timeouts
Example:
// Resource preemption
void resource_preemption() {
if (high_priority_process_needs_resource()) {
preempt_resource_from_low_priority_process();
allocate_resource_to_high_priority_process();
}
}
Breaking Circular Wait
Strategy: number the resources and require processes to request them in increasing order.
Methods:
- Ordered resource allocation
- A resource numbering scheme
- A hierarchical structure of resources
Example:
// Resource numbering: R1=1, R2=2, R3=3
void ordered_resource_allocation() {
// resources can only be requested in number order
acquire_resource(1); // R1
acquire_resource(2); // R2
acquire_resource(3); // R3
// R3 can never be requested before R1
}
Deadlock Avoidance
The Banker’s Algorithm
Principle: before allocating resources, check whether the allocation would lead to an unsafe state; allocate only in a safe state.
Data structures:
int available[RESOURCES]; // available resources
int maximum[PROCESSES][RESOURCES]; // maximum demand
int allocation[PROCESSES][RESOURCES]; // already allocated
int need[PROCESSES][RESOURCES]; // still needed
Safety check:
bool is_safe_state() {
int work[RESOURCES];
bool finish[PROCESSES];
// initialization
for (int i = 0; i < RESOURCES; i++)
work[i] = available[i];
for (int i = 0; i < PROCESSES; i++)
finish[i] = false;
// look for a process that can finish
for (int count = 0; count < PROCESSES; count++) {
bool found = false;
for (int i = 0; i < PROCESSES; i++) {
if (!finish[i] && can_allocate(i, work)) {
// allocate resources to process i
for (int j = 0; j < RESOURCES; j++)
work[j] += allocation[i][j];
finish[i] = true;
found = true;
}
}
if (!found) return false; // unsafe state
}
return true; // safe state
}
Resource allocation algorithm:
bool request_resources(int process_id, int request[]) {
// check that the request does not exceed the need
for (int i = 0; i < RESOURCES; i++) {
if (request[i] > need[process_id][i])
return false;
}
// check that enough resources are available
for (int i = 0; i < RESOURCES; i++) {
if (request[i] > available[i])
return false;
}
// try to allocate the resources
for (int i = 0; i < RESOURCES; i++) {
available[i] -= request[i];
allocation[process_id][i] += request[i];
need[process_id][i] -= request[i];
}
// check whether the state is still safe
if (is_safe_state()) {
return true; // allocation succeeded
} else {
// restore the previous state
for (int i = 0; i < RESOURCES; i++) {
available[i] += request[i];
allocation[process_id][i] -= request[i];
need[process_id][i] += request[i];
}
return false; // allocation failed
}
}
The Resource Allocation Graph Algorithm
Principle: use a directed graph to describe how resources are allocated, and detect whether it contains a cycle.
How the graph is built:
- Nodes: processes and resources
- Edges: allocation and request relationships
Detection algorithm:
bool detect_deadlock() {
// simplify the resource allocation graph
while (a_simplifiable_node_exists()) {
// find a process node that can be simplified
for (each process node P) {
if (all_requests_of(P_can_be_satisfied)) {
// remove all edges of P
remove_all_edges_from(P);
// mark P as simplified
mark_as_simplified(P);
}
}
}
// check whether any edges remain
if (graph_has_remaining_edges())
return true; // a deadlock exists
else
return false; // no deadlock exists
}
Deadlock Detection
The Resource Allocation Graph
How the graph is represented:
- Circular nodes: processes
- Square nodes: resources
- Solid arrows: allocation (resource → process)
- Dashed arrows: request (process → resource)
Example:
The Detection Algorithm
Simplification algorithm:
- Find a process node that has no outgoing request edge
- Remove all of that node’s allocation edges
- Repeat steps 1-2 until nothing more can be simplified
- If edges remain in the graph, a deadlock exists
Implementation:
bool detect_deadlock_using_graph() {
// build the resource allocation graph
build_resource_allocation_graph();
// simplify the graph
while (true) {
bool simplified = false;
// look for a process that can be simplified
for (each process P) {
if (P_has_no_request_edge) {
// remove all allocation edges of P
remove_allocation_edges(P);
simplified = true;
}
}
if (!simplified) break;
}
// check whether any edges remain
return has_remaining_edges();
}
When Detection Runs
Detection policies:
- Periodic detection: check once every so often
- On-demand detection: check whenever a process requests a resource
- Exhaustion detection: check when a resource type is used up
Cost of detection:
- Time complexity: O(n²), where n is the number of processes
- Space complexity: O(n×m), where m is the number of resource types
Deadlock Recovery
Process Termination
Strategy: terminate some or all of the deadlocked processes.
Methods:
- Terminate all deadlocked processes: simple but costly
- Terminate deadlocked processes one at a time: kill the process with the smallest cost
Selection criteria:
- Process priority
- How long the process has already run
- How much time the process still needs
- How many resources the process uses
Example:
void terminate_deadlocked_processes() {
// compute the termination cost of every process
for (each deadlocked process P) {
cost[P] = calculate_termination_cost(P);
}
// terminate the cheapest process each time
while (a_deadlock_exists()) {
process = find_minimum_cost_process();
terminate_process(process);
release_resources(process);
}
}
Resource Preemption
Strategy: preempt resources from some processes and give them to others.
Methods:
- Choose a victim process: pick the process with the smallest cost
- Roll back to a safe state: roll the process back to a checkpoint
- Reallocate the resources: give the preempted resources to other processes
Rollback mechanism:
void resource_preemption() {
// choose a victim process
victim = select_victim_process();
// roll back to the checkpoint
rollback_to_checkpoint(victim);
// release the resources
release_resources(victim);
// redistribute the resources
redistribute_resources();
}
Checkpointing
Principle: save the state of processes periodically, so that a deadlocked process can be rolled back.
Implementation:
// Save a checkpoint
void save_checkpoint(int process_id) {
checkpoint[process_id] = {
.registers = get_process_registers(process_id),
.memory = get_process_memory(process_id),
.resources = get_process_resources(process_id)
};
}
// Roll back to a checkpoint
void rollback_to_checkpoint(int process_id) {
restore_process_state(process_id, checkpoint[process_id]);
}
Comparing Deadlock Handling Strategies
Choosing a Strategy
| Strategy | Advantages | Disadvantages | Suitable for |
|---|---|---|---|
| Prevention | Deadlocks are avoided altogether | Low resource utilization | Real-time systems |
| Avoidance | Higher resource utilization | Resource demands must be known in advance | Batch systems |
| Detection | Highest resource utilization | Detection and recovery are expensive | General-purpose systems |
| Ignore | Simple to implement | May hang the system | Development environments |
Real-World Use
Choices made by operating systems:
- Windows: relies mainly on deadlock detection and recovery
- Linux: relies mainly on deadlock prevention and avoidance
- Real-time systems: rely mainly on deadlock prevention
Factors to consider:
- Type of system: real-time, time-sharing, or batch
- Nature of the resources: how many there are, what types, and how they are allocated
- Performance requirements: response time, throughput, resource utilization
- Reliability requirements: stability of the system and its ability to recover from failures
Deadlock Case Studies
Case 1: The Dining Philosophers Problem
Problem:
- 5 philosophers sit around a round table
- Each philosopher needs two chopsticks to eat
- The chopsticks lie between the philosophers
How the deadlock happens:
All philosophers pick up their left chopstick at the same time
Then they wait for the right chopstick
A circular wait forms
Solution:
// Use ordered resource allocation
void philosopher(int i) {
int left = i;
int right = (i + 1) % 5;
// always request the lower-numbered chopstick first
if (left < right) {
acquire_chopstick(left);
acquire_chopstick(right);
} else {
acquire_chopstick(right);
acquire_chopstick(left);
}
eat();
release_chopstick(left);
release_chopstick(right);
}
Case 2: Database Deadlock
Problem:
- Transaction A holds lock L1 and requests lock L2
- Transaction B holds lock L2 and requests lock L1
Solution:
-- Use a timeout mechanism
SET LOCK_TIMEOUT 5000; -- 5 second timeout
-- Use deadlock detection
SET DEADLOCK_PRIORITY LOW; -- set the deadlock priority
-- Use ordered resource allocation
-- Always request locks in a fixed order
BEGIN TRANSACTION;
UPDATE table1 SET column1 = value1 WHERE id = 1;
UPDATE table2 SET column2 = value2 WHERE id = 1;
COMMIT;
| Term | Abbreviation | Explanation |
|---|---|---|
| Deadlock | - | A state in which processes wait for one another and cannot progress |
| Resource | - | A system resource that a process needs |
| Reusable Resource | - | A resource that can be used again and again |
| Consumable Resource | - | A resource that disappears once it is used |
| Preemptable Resource | - | A resource that can be preempted |
| Nonpreemptable Resource | - | A resource that cannot be preempted |
| Mutual Exclusion | - | One of the necessary conditions for deadlock |
| Hold and Wait | - | One of the necessary conditions for deadlock |
| No Preemption | - | One of the necessary conditions for deadlock |
| Circular Wait | - | One of the necessary conditions for deadlock |
| Deadlock Prevention | - | Breaking the necessary conditions of deadlock |
| Deadlock Avoidance | - | Deciding dynamically whether to avoid a deadlock |
| Deadlock Detection | - | Determining whether a deadlock has occurred |
| Deadlock Recovery | - | Restoring the system after a deadlock occurs |
| Banker’s Algorithm | - | An algorithm for deadlock avoidance |
| Safe State | - | A state in which a safe sequence exists |
| Safe Sequence | - | A sequence of processes that can all finish |
| Resource Allocation Graph | RAG | The graph that describes resource allocation |
| Resource Request Graph | - | The graph that describes resource requests |
| Process Termination | - | Terminating processes to break a deadlock |
| Resource Preemption | - | Preempting resources to break a deadlock |
| Rollback | - | Rolling a process back to a safe state |
| Ostrich Algorithm | - | The policy of ignoring deadlocks |
