Necessary Conditions for Deadlock
Hold and Wait
Read aloud is unavailable in this browser.
The Second Pillar of Deadlock
Mutual exclusion alone cannot cause deadlock. A process that holds an exclusive resource and needs nothing else will eventually complete and release that resource. Deadlock requires something more: processes must hold resources while simultaneously waiting for additional resources. This second necessary condition—hold and wait—describes the resource accumulation pattern that enables deadlock.
Without hold and wait, no process would ever block while holding resources. Without blocked holders, no chain of waiting could form. Without waiting chains, no cycle could emerge. Hold and wait is the condition that transforms isolated resource holdings into interconnected webs of dependency that can entangle processes in permanent stalemate.
By the end of this page, you will deeply understand the hold-and-wait condition: what it means formally, why it arises naturally in concurrent programming, how it interacts with mutual exclusion to enable deadlock, and critically, how it can be prevented through strategic resource acquisition protocols.
The Hold and Wait Condition Defined
Formal Definition:
A process holding at least one resource is waiting to acquire additional resources that are currently held by other processes.
This definition has several critical components:
1. Current Holdings — The process already possesses at least one resource. It is not starting from a resource-free state.
2. Additional Requests — The process needs more resources beyond what it currently holds. Its computation cannot proceed with only its current allocations.
3. Blocking Wait — The additional resources are currently held by other processes, so the requesting process must wait.
4. Retention During Wait — Crucially, the process does not release its currently held resources while waiting. It holds and waits simultaneously.
Coffman et al. stated this condition as: "A process must be holding resources while waiting to acquire additional resources." The conjunction of holding AND waiting is essential—a process that releases its holdings before waiting, or one that holds without needing more, does not satisfy this condition.
Mathematical Formalization:
Let allocation function H(Pᵢ) denote the set of resources held by process Pᵢ, and W(Pᵢ) denote the set of resources Pᵢ is waiting to acquire.
The hold-and-wait condition is satisfied when:
∃Pᵢ: H(Pᵢ) ≠ ∅ ∧ W(Pᵢ) ≠ ∅
That is, there exists a process that has non-empty holdings AND non-empty wait set. The process simultaneously occupies resources and desires more.
State Characterization:
A process Pᵢ can be in one of four states with respect to hold-and-wait:
| Holds Resources | Wants More | Contributes to Hold-and-Wait? |
|---|---|---|
| No | No | No — idle or resource-free |
| No | Yes | No — pure waiter (will block) |
| Yes | No | No — can proceed or will complete |
| Yes | Yes | Yes — hold and wait! |
Why Hold and Wait Occurs
Hold and wait is not an aberration or poor programming practice—it arises naturally and often necessarily from the requirements of concurrent computation. Understanding why it occurs reveals why eliminating it requires significant effort.
- •Incremental Resource Acquisition — Programs often cannot know all resources they'll need until runtime. A process parsing a file may discover it needs a database connection only after examining the file's contents.
- •Efficiency Concerns — Acquiring and releasing resources has overhead. Holding resources across multiple operations is often more efficient than repeatedly acquiring and releasing them.
- •Atomicity Requirements — Many operations require holding multiple resources simultaneously for correctness. Releasing one resource mid-operation would violate transactional semantics.
- •Programming Simplicity — Incremental acquisition is natural to programmers. The alternative—determining all resources upfront—is cognitively demanding and sometimes impossible.
- •Dynamic Workloads — The resources needed often depend on data discovered during execution. A web server handling a request doesn't know if it needs the cache lock until it finds a cache miss.
The Natural Pattern:
Consider a realistic scenario: a process transferring funds between bank accounts.
1. Acquire lock on source account → Process now holds 1 resource
2. Verify sufficient balance → Process still holds 1 resource
3. Acquire lock on destination acc → Process wants 2nd resource
If held by another: HOLD AND WAIT
This pattern is not a programming mistake—it's how transactions must work. The source account lock cannot be released before the destination is verified, or the transfer could partially complete. The process must hold the source lock while attempting to acquire the destination lock.
Hold and wait is often semantically necessary. Banking, inventory management, and database transactions all require holding multiple resources atomically. The goal is not to eliminate hold and wait entirely (often impossible), but to manage it carefully or prevent deadlock through other means.
The Cascade Effect: How Hold and Wait Spreads
Hold and wait doesn't just affect the process in that state—it cascades through the system, potentially blocking many other processes and amplifying the deadlock risk.
Cascade Scenario:
Consider four processes and four resources:
Time T0:
P1: Holds R1, requests R2 → R2 held by P2
P2: Holds R2, runs (not waiting)
P3: Runs (no resources)
P4: Runs (no resources)
Time T1: P2 completes with R2
P1: Acquires R2 (now holds R1, R2)
P2: Exits
P3: Requests R2 → WAIT (R2 held by P1)
P4: Requests R1 → WAIT (R1 held by P1)
Time T2: P1 requests R3
P1: Holds R1, R2, requests R3 → WAIT (R3 held by P5)
P3: Waiting for R2 (held by P1)
P4: Waiting for R1 (held by P1)
Result: P1 in hold-and-wait has blocked P3 and P4
P1's hold-and-wait state has created a chain: P1 → P5, and both P3 and P4 → P1. If P5 happens to wait for a resource held by P3 or P4, a cycle forms instantly.
Every process in a hold-and-wait state becomes a potential link in a deadlock chain. The more processes holding resources while waiting, the more potential edges exist in the resource allocation graph, and the greater the probability that a cycle will eventually form.
Resource Accumulation:
Processes in hold-and-wait tend to accumulate resources over time:
- Start with 0 resources
- Acquire R1 (hold 1)
- Wait for R2, acquire R2 (hold 2)
- Wait for R3, acquire R3 (hold 3) ...
Each acquisition potentially creates wait states for other processes. The longer a process holds resources, the more it blocks other processes, and the greater the cascade effect. This is why long-running transactions are particularly dangerous for deadlock.
Visualizing Hold and Wait
Resource allocation graphs provide a powerful visualization of hold-and-wait relationships. Understanding how to read these graphs is essential for deadlock analysis.
Resource Allocation Graph Notation:
- Circles (Pᵢ): Processes
- Rectangles (Rⱼ): Resource types
- Dots inside rectangles: Resource instances
- Arrow Pᵢ → Rⱼ: Request edge — Pᵢ is waiting for an instance of Rⱼ
- Arrow Rⱼ → Pᵢ: Assignment edge — an instance of Rⱼ is allocated to Pᵢ
Identifying Hold and Wait:
A process is in hold-and-wait if and only if:
- It has at least one incoming assignment edge (holds resource)
- It has at least one outgoing request edge (wants resource)
Resource Allocation Graph Example:═══════════════════════════════════ ┌─────┐ ┌──│ R1 │──┐ │ │ • │ │ Request │ └─────┘ │ Assignment ───────► │ │ ───────► │ ▼ ┌────┴────┐ ┌────────┐ │ P1 │ │ P2 │ └────┬────┘ └────┬───┘ │ │ Assignment │ │ Request ◄─────── │ │ ◄─────── │ ┌─────┐ │ └─►│ R2 │◄─┘ │ • │ └─────┘ ANALYSIS:─────────• P1: Has R2 (incoming assignment), wants R1 (outgoing request) → P1 is in HOLD-AND-WAIT state • P2: Has R1 (incoming assignment), wants R2 (outgoing request) → P2 is in HOLD-AND-WAIT state • Both processes are in hold-and-wait• There is a cycle: P1 → R1 → P2 → R2 → P1• With single-instance resources, this cycle = DEADLOCKGraph Properties and Hold-and-Wait:
In a resource allocation graph:
- Pure holder (no outgoing requests): Process will eventually release resources
- Pure waiter (no held resources): Process will eventually acquire or can be considered non-blocking
- Hold-and-wait (both edges): Dangerous state — potential deadlock participant
The presence of hold-and-wait states is a prerequisite for deadlock cycles. Without hold-and-wait, either:
- All request edges would originate from pure waiters (no cycle possible through them), or
- All holders would have no request edges (they complete and release)
Hold-and-wait creates the possibility for request chains to pass through holding processes, enabling cycles.
Real-World Hold and Wait Scenarios
Hold and wait manifests throughout software systems. Understanding common patterns helps recognize and prevent deadlock risks.
Scenario 1: Database Transactions
-- Transaction T1
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- Holds row lock on account 1
-- Processing...
SELECT * FROM accounts WHERE id = 2 FOR UPDATE; -- Requests lock on account 2
-- If T2 holds it: HOLD AND WAIT
-- Transaction T2
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 2 FOR UPDATE; -- Holds row lock on account 2
-- Processing...
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- Requests lock on account 1
-- If T1 holds it: HOLD AND WAIT
Both transactions are in hold-and-wait. If they proceed concurrently with this ordering, deadlock results.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
import threadingimport time # File locks simulated with Python lockslock_config = threading.Lock()lock_data = threading.Lock() def updater_a(): """Updates data based on configuration""" # Step 1: Acquire config lock to read configuration with lock_config: # HOLDS lock_config print("Updater A: Acquired config lock") config = read_config() time.sleep(0.1) # Simulate processing # Step 2: Need data lock to update data # HOLD AND WAIT: Holds lock_config, waits for lock_data with lock_data: print("Updater A: Acquired data lock") update_data(config) def updater_b(): """Updates configuration based on data""" # Step 1: Acquire data lock to read data with lock_data: # HOLDS lock_data print("Updater B: Acquired data lock") data = read_data() time.sleep(0.1) # Simulate processing # Step 2: Need config lock to update config # HOLD AND WAIT: Holds lock_data, waits for lock_config with lock_config: print("Updater B: Acquired config lock") update_config(data) # If run concurrently, these threads may deadlock# Thread A: holds config, waits for data# Thread B: holds data, waits for config# DEADLOCK! thread_a = threading.Thread(target=updater_a)thread_b = threading.Thread(target=updater_b) thread_a.start()thread_b.start() # These joins may never completethread_a.join()thread_b.join()Scenario 3: Operating System Kernel
The kernel frequently encounters hold-and-wait:
Process: Page fault handler
1. Holds: Address space lock (to prevent concurrent mmap)
2. Needs: Free page from physical memory allocator
3. Allocator locked by memory reclaimer
Reclaimer:
1. Holds: Allocator lock (modifying free page list)
2. Needs: Address space lock (to unmap pages)
→ Potential deadlock if not carefully ordered
Kernel developers must meticulously analyze resource acquisition patterns to prevent such scenarios. Lock ordering rules ("always acquire the address space lock before the allocator lock") are essential in kernel code.
The pattern of acquiring locks while holding other locks—nested or hierarchical lock acquisition—is the quintessential hold-and-wait pattern. Every nested lock acquisition is a potential deadlock setup. Careful lock ordering and design is essential.
Preventing Hold and Wait
Unlike mutual exclusion, hold and wait can often be prevented through careful protocol design. Eliminating this condition is one of the primary strategies for deadlock prevention.
- •All-at-Once Acquisition — Require processes to request all needed resources before execution begins. If all resources are available, grant them all; otherwise, grant none and make the process wait. This ensures no process ever holds while waiting.
- •Release Before Request — Before requesting a new resource, a process must release all currently held resources. It then requests the resources it still needs (including the released ones). This ensures the process is never holding while waiting.
- •Two-Phase Protocol — Phase 1 (Growing): Acquire all needed resources without releasing any. Phase 2 (Shrinking): Release resources without acquiring new ones. A process can never be in hold-and-wait because once it starts releasing, it never acquires again.
- •Lock Timeout with Retry — If a process cannot acquire a resource within a timeout, it releases all held resources and retries from the beginning. This breaks the static hold-and-wait pattern through dynamic release.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
#include <pthread.h>#include <stdbool.h> // Global resource managerpthread_mutex_t resource_manager_lock = PTHREAD_MUTEX_INITIALIZER; // Resource availability arraybool resources[MAX_RESOURCES];int num_resources; // Request all resources at oncebool request_all_resources(int process_id, int *needed, int count) { pthread_mutex_lock(&resource_manager_lock); // Check if ALL needed resources are available for (int i = 0; i < count; i++) { if (!resources[needed[i]]) { // At least one resource unavailable - request denied pthread_mutex_unlock(&resource_manager_lock); return false; // Process must wait and retry } } // All resources available - allocate them all atomically for (int i = 0; i < count; i++) { resources[needed[i]] = false; // Mark as allocated printf("Process %d: Acquired resource %d", process_id, needed[i]); } pthread_mutex_unlock(&resource_manager_lock); return true; // Process received all resources, can proceed} // Release all resources at oncevoid release_all_resources(int process_id, int *held, int count) { pthread_mutex_lock(&resource_manager_lock); for (int i = 0; i < count; i++) { resources[held[i]] = true; // Mark as available printf("Process %d: Released resource %d", process_id, held[i]); } pthread_mutex_unlock(&resource_manager_lock); // Signal waiting processes to retry their requests broadcast_resource_available();} // Usage example - no hold-and-wait possiblevoid process_work(int process_id) { int needed[] = {RESOURCE_A, RESOURCE_B, RESOURCE_C}; int count = 3; // Keep trying until all resources obtained while (!request_all_resources(process_id, needed, count)) { // Wait and retry - no resources held during wait wait_for_resources(); } // Now holds all needed resources - do work do_critical_work(); // Release all at once release_all_resources(process_id, needed, count);}Trade-offs of Preventing Hold and Wait:
| Strategy | Advantages | Disadvantages |
|---|---|---|
| All-at-Once | Simple; guarantees no hold-and-wait | May not know resources needed upfront; reduces resource utilization; possible starvation |
| Release Before Request | Flexible; can adapt to runtime needs | Complex rollback; wasted work; possible livelock |
| Two-Phase Protocol | Clear structure; proven safe | Requires planning phases; not always applicable |
| Timeout with Retry | Breaks deadlock; self-healing | Wasted computation; complex retry logic; possible livelock |
Practical Challenges in Preventing Hold and Wait
While preventing hold and wait is theoretically attractive, practical implementation faces significant challenges that explain why it's not universally applied.
- •Unknown Future Needs — Many programs cannot predict their resource requirements until runtime. A compiler doesn't know how many temporary files it needs until it parses the source code.
- •Over-Allocation — Requesting all potentially needed resources upfront leads to over-allocation. A process that might need a network connection holds it even if the code path requiring it is never executed.
- •Reduced Concurrency — Holding resources longer than strictly necessary (waiting for all to be available) reduces system concurrency and throughput.
- •Starvation Risk — If a process needs many resources, it may never find them all simultaneously available, leading to indefinite waiting (starvation).
- •Rollback Complexity — Release-before-request strategies require process state to be rolled back, which is complex or impossible for I/O operations.
- •Work Loss — Releasing resources and re-acquiring them wastes computation performed while holding the resources.
The All-at-Once Dilemma:
Consider a database query processor:
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.region = 'WEST'
The optimizer determines the execution plan:
- Scan customers index for region='WEST'
- For each matching customer, probe orders table
What resources are needed?
- Lock on customers table? (Maybe partial, depends on data)
- Lock on orders table? (Depends on matching customer IDs)
- Memory for hash join? (Depends on customer count)
- Disk I/O buffers? (Depends on buffer pool state)
The actual resources needed depend on data content, statistics, and runtime conditions. Declaring all resources upfront would require massive over-allocation or special query planning.
In practice, systems often accept hold-and-wait but prevent deadlock through other means: lock ordering (breaking circular wait), timeout and retry with intelligent backoff, or deadlock detection and recovery. Hold-and-wait prevention is valuable for specific subsystems with predictable resource needs.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.