Loading content...
Deadlock manifests as a cycle in the Resource Allocation Graph—a closed loop where every process in the cycle is waiting for a resource held by the next process. Cycle detection is therefore the algorithmic heart of deadlock detection for single-instance resource systems.
But how do we efficiently find cycles in a graph? This page explores the classic depth-first search approach, its application to RAGs, the derivation of wait-for graphs that simplify detection, and the time and space complexity considerations that matter for real systems.
For single-instance resources, finding a cycle is finding a deadlock. For multi-instance resources, it's a necessary first step before more sophisticated analysis.
By the end of this page, you will understand: depth-first search for cycle detection, coloring schemes to track visit state, the wait-for graph simplification, complexity analysis, and when to trigger detection versus continuous monitoring.
Depth-First Search (DFS) is the classic algorithm for cycle detection in directed graphs. The key insight is that a cycle exists if and only if we encounter a vertex that is currently on the DFS recursion stack—a back edge.
Algorithm Overview:
Why This Works:
A vertex colored GRAY is currently on the path we're exploring. If an edge leads to a GRAY vertex, we've found a path that loops back to itself—the definition of a cycle.
A vertex colored BLACK has been fully explored; its subtree has no cycles that we haven't already detected.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
"""DFS-based Cycle Detection for Directed Graphs""" from enum import Enumfrom typing import Dict, List, Optional, Set, Tuple class Color(Enum): WHITE = 0 # Unvisited GRAY = 1 # Currently visiting (on recursion stack) BLACK = 2 # Finished def detect_cycle_dfs(graph: Dict[str, List[str]]) -> Optional[List[str]]: """ Detect a cycle in a directed graph using DFS. Args: graph: Adjacency list representation {vertex: [neighbors]} Returns: List of vertices forming a cycle, or None if no cycle exists. """ color: Dict[str, Color] = {v: Color.WHITE for v in graph} parent: Dict[str, Optional[str]] = {v: None for v in graph} def dfs(vertex: str, path: List[str]) -> Optional[List[str]]: """DFS with cycle detection and path tracking.""" color[vertex] = Color.GRAY path.append(vertex) for neighbor in graph.get(vertex, []): if color[neighbor] == Color.GRAY: # Back edge found - extract cycle cycle_start = path.index(neighbor) return path[cycle_start:] + [neighbor] if color[neighbor] == Color.WHITE: result = dfs(neighbor, path) if result: return result path.pop() color[vertex] = Color.BLACK return None # Try DFS from each unvisited vertex for vertex in graph: if color[vertex] == Color.WHITE: cycle = dfs(vertex, []) if cycle: return cycle return None # No cycle found # Example usage with a simple graphdef demonstrate_cycle_detection(): # Graph with cycle: A -> B -> C -> A graph_with_cycle = { 'A': ['B'], 'B': ['C'], 'C': ['A'], # Back edge creates cycle 'D': ['E'], 'E': [] } cycle = detect_cycle_dfs(graph_with_cycle) print(f"Cycle found: {cycle}") # ['A', 'B', 'C', 'A'] # Graph without cycle graph_no_cycle = { 'A': ['B', 'C'], 'B': ['D'], 'C': ['D'], 'D': [] } cycle = detect_cycle_dfs(graph_no_cycle) print(f"Cycle found: {cycle}") # None if __name__ == "__main__": demonstrate_cycle_detection()| Color | Meaning | Implication of Edge to This Color |
|---|---|---|
| WHITE | Unvisited | Tree edge - explore it |
| GRAY | In current path (on stack) | Back edge - CYCLE FOUND |
| BLACK | Fully explored | Forward/cross edge - no new cycle |
Applying DFS to a RAG requires understanding the graph's bipartite structure. Cycles in a RAG alternate between process and resource vertices.
RAG Edge Traversal:
From a process P:
From a resource R:
The Alternating Pattern:
A cycle in a RAG has the form:
P₁ → R₁ → P₂ → R₂ → ... → Pₖ → Rₖ → P₁
The length is always even (alternating P and R), and the cycle includes exactly as many processes as resources.
Adaptation of DFS:
We can simplify the search by focusing only on processes:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
def detect_cycle_in_rag(rag: 'ResourceAllocationGraph') -> Optional[List[str]]: """ Detect a cycle in a Resource Allocation Graph. This implementation traverses the bipartite graph by following request edges (P→R) and assignment edges (R→P). Returns: List of process names in the cycle, or None. """ def get_blocking_processes(process_name: str) -> List[str]: """ Get processes that are blocking this process. Process P1 is blocked by P2 if: - P1 requests some resource R - P2 holds an instance of R """ blocking = set() process = rag.processes[process_name] for resource_name, count in process.requested_resources.items(): if count > 0: resource = rag.resources[resource_name] for holder, held_count in resource.allocated_instances.items(): if held_count > 0 and holder != process_name: blocking.add(holder) return list(blocking) # Standard DFS with colors color = {p: Color.WHITE for p in rag.processes} def dfs(proc: str, path: List[str]) -> Optional[List[str]]: color[proc] = Color.GRAY path.append(proc) for blocking_proc in get_blocking_processes(proc): if color[blocking_proc] == Color.GRAY: # Cycle found cycle_start = path.index(blocking_proc) return path[cycle_start:] if color[blocking_proc] == Color.WHITE: result = dfs(blocking_proc, path) if result: return result path.pop() color[proc] = Color.BLACK return None # DFS from each process for process in rag.processes: if color[process] == Color.WHITE: cycle = dfs(process, []) if cycle: return cycle return None def get_full_rag_cycle(rag, process_cycle: List[str]) -> List[str]: """ Expand a process-only cycle to include resources. Input: ['P1', 'P2', 'P3'] Output: ['P1', 'R1', 'P2', 'R2', 'P3', 'R3', 'P1'] """ full_cycle = [] for i, proc in enumerate(process_cycle): full_cycle.append(proc) next_proc = process_cycle[(i + 1) % len(process_cycle)] # Find the resource connecting proc to next_proc process = rag.processes[proc] for resource_name in process.requested_resources: resource = rag.resources[resource_name] if next_proc in resource.allocated_instances: full_cycle.append(resource_name) break full_cycle.append(process_cycle[0]) # Complete the cycle return full_cycleFor single-instance resources, we can derive a simpler graph that makes cycle detection more direct: the Wait-For Graph.
Definition:
The wait-for graph G' = (P, E') contains:
Construction:
For each process pair (Pᵢ, Pⱼ):
Add edge Pᵢ → Pⱼ iff ∃ resource R such that:
- Pᵢ → R is a request edge in the RAG, AND
- R → Pⱼ is an assignment edge in the RAG
Why This Works (Single-Instance Only):
For single-instance resources, each resource has at most one assignment edge. If Pᵢ requests R and R is assigned to Pⱼ, then Pᵢ is definitively waiting for Pⱼ.
Advantages of Wait-For Graph:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
class WaitForGraph: """ A simplified graph showing only wait-for relationships between processes. Derived from a RAG. Note: Only valid for single-instance resources! """ def __init__(self): self.edges: Dict[str, Set[str]] = {} @classmethod def from_rag(cls, rag: 'ResourceAllocationGraph') -> 'WaitForGraph': """ Construct a wait-for graph from a RAG. For each request edge P→R and assignment edge R→Q (where P≠Q), add edge P→Q to the wait-for graph. """ wfg = cls() # Initialize edges for all processes for process in rag.processes: wfg.edges[process] = set() # Build wait-for edges for process_name, process in rag.processes.items(): for resource_name, count in process.requested_resources.items(): if count > 0: resource = rag.resources[resource_name] # Find who holds this resource for holder, held in resource.allocated_instances.items(): if held > 0 and holder != process_name: wfg.edges[process_name].add(holder) return wfg def detect_cycle(self) -> Optional[List[str]]: """Detect a cycle using DFS.""" color = {p: Color.WHITE for p in self.edges} def dfs(vertex: str, path: List[str]) -> Optional[List[str]]: color[vertex] = Color.GRAY path.append(vertex) for neighbor in self.edges.get(vertex, []): if color[neighbor] == Color.GRAY: cycle_start = path.index(neighbor) return path[cycle_start:] if color[neighbor] == Color.WHITE: result = dfs(neighbor, path) if result: return result path.pop() color[vertex] = Color.BLACK return None for vertex in self.edges: if color[vertex] == Color.WHITE: cycle = dfs(vertex, []) if cycle: return cycle return None def __str__(self): lines = ["Wait-For Graph:"] for proc, waits_for in sorted(self.edges.items()): if waits_for: lines.append(f" {proc} waits for: {', '.join(sorted(waits_for))}") return "".join(lines) # Example: RAG to Wait-For Graph transformationdef demonstrate_wait_for_graph(): # Create RAG with deadlock rag = ResourceAllocationGraph() rag.add_process("P1") rag.add_process("P2") rag.add_process("P3") rag.add_resource("R1", 1) rag.add_resource("R2", 1) rag.add_resource("R3", 1) # Cycle: P1->R1->P2->R2->P3->R3->P1 rag.add_assignment_edge("R1", "P2") rag.add_assignment_edge("R2", "P3") rag.add_assignment_edge("R3", "P1") rag.add_request_edge("P1", "R1") rag.add_request_edge("P2", "R2") rag.add_request_edge("P3", "R3") # Convert to wait-for graph wfg = WaitForGraph.from_rag(rag) print(wfg) # Output: # Wait-For Graph: # P1 waits for: P2 # P2 waits for: P3 # P3 waits for: P1 cycle = wfg.detect_cycle() print(f"Cycle: {cycle}") # ['P1', 'P2', 'P3']The wait-for graph provides a correct deadlock characterization ONLY for single-instance resources. With multi-instance resources, a cycle in the wait-for graph does not necessarily imply deadlock because additional instances might satisfy waiting processes without conflict.
Understanding the computational cost of cycle detection is essential for deciding when and how often to perform it.
Time Complexity:
DFS visits each vertex once and each edge once:
RAG: O(|P| + |R| + |E|) = O(n + m + e)
Wait-For Graph: O(|P| + |E'|) = O(n + n²) = O(n²)
Space Complexity:
Total: O(n + m) for RAG, O(n) for wait-for graph
Practical Considerations:
In real systems with hundreds of processes and resources:
| Metric | RAG Direct | Wait-For Graph | Notes |
|---|---|---|---|
| Vertices | n + m | n | WFG eliminates resources |
| Edges (worst) | n × m | n² | Depends on request patterns |
| Time | O(n + m + e) | O(n + e') | e' typically << e |
| Space | O(n + m) | O(n) | Color + stack |
| Derivation cost | N/A | O(e) | One-time or incremental |
When should the system check for deadlocks? Several strategies exist, each with tradeoffs.
Strategy 1: On Every Request (Eager)
Check for cycles immediately when a request edge is added.
Strategy 2: Periodic (Lazy)
Run detection at regular intervals (every N seconds or M requests).
Strategy 3: On Symptom (Reactive)
Check only when deadlock symptoms appear (e.g., timeout, watchdog).
Strategy 4: Incremental (Hybrid)
Maintain partial analysis state, update incrementally.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
/** * Deadlock Detection Strategy Implementations */ /* Strategy 1: Eager - Check on every request */int resource_request_eager(struct resource *res, struct task_struct *task){ spin_lock(&res->lock); if (res->available > 0) { /* Fast path: grant immediately */ res->available--; rag_add_assignment_edge(res, task); spin_unlock(&res->lock); return 0; } /* Would-block path: check for deadlock first */ rag_add_request_edge(task, res); if (rag_detect_cycle_from(task)) { /* Would cause deadlock - reject request */ rag_remove_request_edge(task, res); spin_unlock(&res->lock); return -EDEADLK; } /* Safe to wait */ add_to_wait_queue(&res->waiters, task); set_current_state(TASK_UNINTERRUPTIBLE); spin_unlock(&res->lock); schedule(); return 0;} /* Strategy 2: Periodic - Background detection thread */static int deadlock_detector_thread(void *data){ while (!kthread_should_stop()) { /* Sleep for detection interval */ schedule_timeout_interruptible(DETECTION_INTERVAL_JIFFIES); /* Acquire RAG snapshot lock */ spin_lock(&rag_lock); /* Run full cycle detection */ struct deadlock_info *info = rag_detect_all_cycles(); spin_unlock(&rag_lock); if (info) { /* Handle detected deadlock */ handle_deadlock(info); free_deadlock_info(info); } } return 0;} /* Strategy 3: Reactive - Check on timeout */int resource_request_with_timeout(struct resource *res, struct task_struct *task, unsigned long timeout_jiffies){ unsigned long deadline = jiffies + timeout_jiffies; spin_lock(&res->lock); /* ... usual request handling ... */ while (res->available == 0) { if (time_after(jiffies, deadline)) { /* Timeout - possible deadlock symptom */ /* Check for deadlock */ if (rag_detect_cycle_from(task)) { handle_deadlock_victim(task); } /* Remove from wait queue */ remove_from_wait_queue(&res->waiters, task); rag_remove_request_edge(task, res); spin_unlock(&res->lock); return -ETIMEDOUT; } spin_unlock(&res->lock); schedule_timeout(deadline - jiffies); spin_lock(&res->lock); } /* ... grant resource ... */ spin_unlock(&res->lock); return 0;}Once a cycle is detected, the system must take action. For single-instance resources, a cycle means definite deadlock. Options include:
Option 1: Process Termination
Select one or more processes in the cycle to terminate:
Option 2: Resource Preemption
Forcibly take resources from some processes:
Option 3: Rollback to Checkpoint
If processes support checkpointing:
Option 4: Operator Intervention
In some systems, alert a human operator:
You now understand cycle detection algorithms for RAGs, the wait-for graph simplification, and strategies for when to perform detection. The final page will tie everything together with deadlock identification in complete systems.