Loading content...
In March 2017, a major cloud provider experienced a cascading failure that affected thousands of websites. The root cause? A deadlock in their distributed locking system that went undetected until it was too late.
Deadlock detection is perhaps the most critical application of cycle detection algorithms. When concurrent processes compete for shared resources, cycles in the resource allocation graph mean processes are waiting for each other indefinitely—a deadlock that can bring entire systems to a halt.
By the end of this page, you will understand how cycle detection applies to deadlock detection in operating systems, dependency resolution in package managers, build system validation, and database transaction management.
A deadlock occurs when a set of processes are blocked, each waiting for a resource held by another process in the set. Four conditions must ALL be present for deadlock:
The fourth condition—circular wait—is where cycle detection becomes essential. If we can detect cycles in the resource/process graph, we can detect deadlocks.
| Node Type | Represents | Graph Role |
|---|---|---|
| Process Node | An executing process/thread | Can request or hold resources |
| Resource Node | A system resource (lock, file, etc.) | Can be held by one process |
| Request Edge | Process → Resource | Process is waiting for this resource |
| Assignment Edge | Resource → Process | Resource is held by this process |
For single-instance resources (each resource has exactly one unit), we can simplify to a Wait-For Graph where:
A cycle in the wait-for graph directly indicates deadlock. If P₁ waits for P₂, P₂ waits for P₃, and P₃ waits for P₁, none can proceed.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
from collections import defaultdictfrom typing import Optionalfrom enum import Enum class Color(Enum): WHITE = 0 GRAY = 1 BLACK = 2 class DeadlockDetector: """ Detects deadlocks using wait-for graph cycle detection. """ def __init__(self): # Resource held by which process: resource_id -> process_id self.resource_holder: dict[str, str] = {} # Resources requested by process: process_id -> set of resource_ids self.waiting_for: dict[str, set[str]] = defaultdict(set) # All known processes self.processes: set[str] = set() def acquire(self, process: str, resource: str) -> bool: """ Process attempts to acquire resource. Returns True if successful, False if must wait. """ self.processes.add(process) if resource not in self.resource_holder: # Resource is free self.resource_holder[resource] = process self.waiting_for[process].discard(resource) return True elif self.resource_holder[resource] == process: # Already holds it return True else: # Must wait self.waiting_for[process].add(resource) return False def release(self, process: str, resource: str) -> None: """Process releases resource.""" if self.resource_holder.get(resource) == process: del self.resource_holder[resource] def _build_wait_for_graph(self) -> dict[str, list[str]]: """ Build wait-for graph from current state. Edge P1 -> P2 means P1 is waiting for resource held by P2. """ graph: dict[str, list[str]] = {p: [] for p in self.processes} for process, resources in self.waiting_for.items(): for resource in resources: if resource in self.resource_holder: holder = self.resource_holder[resource] if holder != process: graph[process].append(holder) return graph def detect_deadlock(self) -> Optional[list[str]]: """ Detect deadlock by finding cycle in wait-for graph. Returns the cycle (list of processes) if deadlock exists. """ graph = self._build_wait_for_graph() color = {p: Color.WHITE for p in self.processes} parent = {p: None for p in self.processes} def dfs(node: str) -> Optional[list[str]]: color[node] = Color.GRAY for neighbor in graph.get(node, []): if color[neighbor] == Color.GRAY: # Cycle found - reconstruct cycle = [neighbor, node] current = node while parent[current] and parent[current] != neighbor: current = parent[current] cycle.append(current) cycle.append(neighbor) return cycle[::-1] elif color[neighbor] == Color.WHITE: parent[neighbor] = node result = dfs(neighbor) if result: return result color[node] = Color.BLACK return None for process in self.processes: if color[process] == Color.WHITE: cycle = dfs(process) if cycle: return cycle return None # Example: Classic dining philosophers deadlockdetector = DeadlockDetector() # Each philosopher tries to pick up left fork, then right forkdetector.acquire("P1", "Fork1") # P1 gets Fork1detector.acquire("P2", "Fork2") # P2 gets Fork2detector.acquire("P1", "Fork2") # P1 waits for Fork2 (held by P2)detector.acquire("P2", "Fork1") # P2 waits for Fork1 (held by P1) deadlock = detector.detect_deadlock()print(f"Deadlock detected: {deadlock}") # ['P1', 'P2', 'P1']Package managers (npm, pip, cargo, maven) must resolve dependencies before installation. A circular dependency—package A requires B, B requires C, C requires A—makes installation impossible.
Cycle detection ensures dependencies can be resolved in a valid order (topological sort).
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
from collections import defaultdict, dequefrom typing import Optional class DependencyResolver: """ Resolves package dependencies and detects circular dependencies. """ def __init__(self): self.dependencies: dict[str, list[str]] = defaultdict(list) def add_package(self, package: str, deps: list[str]) -> None: """Add package with its dependencies.""" self.dependencies[package] = deps for dep in deps: if dep not in self.dependencies: self.dependencies[dep] = [] def resolve(self) -> tuple[Optional[list[str]], Optional[list[str]]]: """ Returns (install_order, None) if resolvable, or (None, cycle) if circular dependency exists. """ packages = list(self.dependencies.keys()) in_degree = {p: 0 for p in packages} # Build reverse graph and compute in-degrees for package, deps in self.dependencies.items(): for dep in deps: in_degree[package] += 1 # Start with packages that have no dependencies queue = deque([p for p in packages if in_degree[p] == 0]) install_order = [] while queue: package = queue.popleft() install_order.append(package) # Packages that depend on this one for pkg, deps in self.dependencies.items(): if package in deps: in_degree[pkg] -= 1 if in_degree[pkg] == 0: queue.append(pkg) if len(install_order) == len(packages): return install_order, None # Find cycle among remaining packages remaining = [p for p in packages if in_degree[p] > 0] cycle = self._find_cycle(remaining) return None, cycle def _find_cycle(self, packages: list[str]) -> list[str]: """Find a cycle among the given packages.""" WHITE, GRAY, BLACK = 0, 1, 2 color = {p: WHITE for p in packages} parent = {} def dfs(node: str) -> Optional[list[str]]: color[node] = GRAY for dep in self.dependencies[node]: if dep in color: if color[dep] == GRAY: cycle = [dep, node] curr = node while parent.get(curr) and parent[curr] != dep: curr = parent[curr] cycle.append(curr) return cycle[::-1] elif color[dep] == WHITE: parent[dep] = node result = dfs(dep) if result: return result color[node] = BLACK return None for pkg in packages: if color[pkg] == WHITE: cycle = dfs(pkg) if cycle: return cycle return [] # Exampleresolver = DependencyResolver()resolver.add_package("webapp", ["react", "utils"])resolver.add_package("react", ["prop-types"])resolver.add_package("utils", ["lodash"])resolver.add_package("lodash", [])resolver.add_package("prop-types", []) order, cycle = resolver.resolve()print(f"Install order: {order}") # Valid order # Add circular dependencyresolver.add_package("core", ["webapp"]) # webapp depends on utilsresolver.dependencies["utils"].append("core") # Creates cycle order, cycle = resolver.resolve()print(f"Circular dependency: {cycle}")Build systems (Make, Bazel, CMake) use directed graphs to represent build targets and their dependencies. Before compilation, they must:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
class BuildSystem: """Simplified build system with cycle detection.""" def __init__(self): self.targets: dict[str, list[str]] = {} def add_target(self, name: str, deps: list[str]) -> None: self.targets[name] = deps def validate_and_order(self) -> tuple[bool, list[str]]: """ Validate build graph and return build order. Returns (success, order_or_cycle). """ WHITE, GRAY, BLACK = 0, 1, 2 color = {t: WHITE for t in self.targets} order = [] def dfs(target: str) -> bool: color[target] = GRAY for dep in self.targets.get(target, []): if dep not in color: continue # External dependency if color[dep] == GRAY: return False # Cycle! if color[dep] == WHITE and not dfs(dep): return False color[target] = BLACK order.append(target) return True for target in self.targets: if color[target] == WHITE: if not dfs(target): return False, [] return True, order # order is in dependency-first order # Example: C++ projectbuild = BuildSystem()build.add_target("main.o", ["utils.o", "network.o"])build.add_target("utils.o", ["common.o"])build.add_target("network.o", ["common.o"])build.add_target("common.o", []) valid, order = build.validate_and_order()print(f"Build order: {order}") # ['common.o', 'utils.o', 'network.o', 'main.o']Database systems use cycle detection for deadlock detection among concurrent transactions:
Databases typically run deadlock detection periodically or on-demand, choosing a victim transaction to abort and break the cycle.
| Strategy | Approach | Trade-off |
|---|---|---|
| Prevention | Order resource acquisition | Limits concurrency |
| Avoidance | Don't grant if may deadlock | Requires future knowledge |
| Detection | Periodic cycle detection | Recovery overhead |
| Timeout | Abort after waiting too long | May abort needlessly |
You've mastered cycle detection in both undirected and directed graphs, understood the DFS state-marking paradigm, and seen how these concepts apply to critical real-world systems. This knowledge is foundational for system design, concurrency, and build tooling.