deadlocks
Deadlocks
Learn how circular resource dependencies can permanently block concurrent work, how to identify the four necessary deadlock conditions, and how lock ordering, bounded waiting, avoidance, detection and recovery reduce deadlock risk.
Introduction
Synchronization protects shared state, but incorrect synchronization can prevent a concurrent application from making progress.
A deadlock occurs when a group of threads, processes or transactions waits indefinitely because each participant requires a resource or action that another blocked participant must provide.
Deadlocks can involve:
- Mutexes and read-write locks
- Semaphores and resource permits
- Database row or table locks
- File locks
- Connection pools
- Thread-pool tasks
- Message exchanges
- Distributed services
- Administrative or deployment operations
Core idea: A deadlocked program may remain running while the affected work permanently stops progressing. Preventing deadlocks requires control over resource ownership, acquisition order and waiting behaviour.
In your System Design curriculum, Deadlocks is Topic 2.5 under Computer Systems, Linux and Concurrency. It follows synchronization and race conditions, and prepares learners to reason about files, sockets and Linux diagnostic tools.
Prerequisites
| # | Prerequisite | Why It Is Needed |
|---|---|---|
| 1 | Processes versus threads | Deadlocks can involve several schedulable execution contexts. |
| 2 | Synchronization | Mutexes, semaphores and condition variables can participate in waiting cycles. |
| 3 | Race conditions | Concurrency correctness requires preventing both unsafe overlap and permanent blocking. |
| 4 | Critical sections | Locks are commonly acquired around operations involving shared invariants. |
| 5 | Basic debugging | Deadlock investigation requires examining blocked threads and resource ownership. |
What Is a Deadlock?
A deadlock is a state in which a set of participants cannot continue because each participant is waiting for a resource or event that another participant in the same blocked set must release or produce.
Two-thread Example
Thread A Thread B
-------- --------
Acquire Lock A Acquire Lock B
Wait for Lock B Wait for Lock A
| |
+-------------+-------------+
|
v
Deadlock
Neither thread can acquire its second lock. Neither thread reaches the code that releases its first lock.
Deadlock vs Other Concurrency Problems
| Problem | Description | Typical Symptom |
|---|---|---|
| Deadlock | Participants wait in a dependency cycle and cannot progress | Work remains blocked indefinitely |
| Race condition | Correctness depends on an uncontrolled execution ordering | Incorrect or inconsistent results |
| Starvation | A participant repeatedly fails to obtain needed resources | One task makes no progress while others continue |
| Livelock | Participants remain active but repeatedly react without completing useful work | High activity with no completion |
| Long lock wait | A participant waits for a busy resource that will eventually become available | High latency, but eventual progress |
Four Necessary Deadlock Conditions
Four conditions must hold simultaneously for a classic resource deadlock to occur:
- Mutual exclusion
- Hold and wait
- No preemption
- Circular wait
Prevention principle: If the design guarantees that at least one necessary condition cannot occur, that class of deadlock cannot form.
Mutual Exclusion
Mutual exclusion exists when a resource can be held by only one participant at a time.
Lock A
|
+--> Owned by Thread A
Thread B requests Lock A
|
+--> Thread B must wait
Many resources require exclusive access while being modified. Therefore, eliminating mutual exclusion is not always possible.
Mutual exclusion can sometimes be reduced through:
- Immutable data
- Read-only sharing
- Per-thread state
- Partitioned resources
- Message passing
- Concurrent read access through an appropriate read-write design
Hold and Wait
Hold and wait occurs when a participant retains one resource while waiting to acquire another.
Thread A:
Hold Lock A
|
v
Wait for Lock B
Possible prevention strategies include:
- Acquire all required resources together when practical.
- Release currently held resources before requesting additional ones.
- Redesign the operation to require fewer simultaneous resources.
- Copy required state and release the first lock before performing other work.
Acquiring every possible resource in advance can reduce concurrency and may be impractical when required resources are discovered dynamically.
No Preemption
No preemption means a resource is not forcibly removed from its owner. The owner must release it according to the resource protocol.
Thread A owns Lock A.
Thread B cannot forcibly take Lock A.
Thread B waits until Thread A releases it.
Mutex ownership normally follows this model. Some other resources can support cancellation, rollback, lease expiry or transaction termination, which can provide a form of recovery.
Safety warning: Forcibly taking a synchronization resource can expose partially updated state. Recovery must preserve the protected invariant.
Circular Wait
Circular wait occurs when each participant waits for a resource held by the next participant in a cycle.
Thread A waits for Lock B
^ |
| v
Lock A held by A Lock B held by B
| ^
v |
Thread B waits for Lock A
A consistent global resource-acquisition order is one of the most practical ways to prevent circular wait.
Classic POSIX Mutex Deadlock
The following program intentionally demonstrates an unsafe lock-ordering design. It must not be used as a correct concurrency pattern.
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
static pthread_mutex_t first_mutex =
PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t second_mutex =
PTHREAD_MUTEX_INITIALIZER;
static void short_delay(
void)
{
struct timespec duration = {
.tv_sec = 0,
.tv_nsec = 100000000L
};
nanosleep(
&duration,
NULL
);
}
static void *first_worker(
void *argument)
{
(void)argument;
pthread_mutex_lock(
&first_mutex
);
puts(
"First worker acquired first mutex."
);
short_delay();
puts(
"First worker is waiting "
"for second mutex."
);
pthread_mutex_lock(
&second_mutex
);
pthread_mutex_unlock(
&second_mutex
);
pthread_mutex_unlock(
&first_mutex
);
return NULL;
}
static void *second_worker(
void *argument)
{
(void)argument;
pthread_mutex_lock(
&second_mutex
);
puts(
"Second worker acquired second mutex."
);
short_delay();
puts(
"Second worker is waiting "
"for first mutex."
);
pthread_mutex_lock(
&first_mutex
);
pthread_mutex_unlock(
&first_mutex
);
pthread_mutex_unlock(
&second_mutex
);
return NULL;
}
int main(void)
{
pthread_t first_thread;
pthread_t second_thread;
if (pthread_create(
&first_thread,
NULL,
first_worker,
NULL) != 0) {
return EXIT_FAILURE;
}
if (pthread_create(
&second_thread,
NULL,
second_worker,
NULL) != 0) {
pthread_join(
first_thread,
NULL
);
return EXIT_FAILURE;
}
pthread_join(
first_thread,
NULL
);
pthread_join(
second_thread,
NULL
);
return EXIT_SUCCESS;
}
Compile
cc -std=c17 -Wall -Wextra -Wpedantic -pthread deadlock_example.c -o deadlock_example
Why Deadlock Can Occur
- The first worker acquires the first mutex.
- The second worker acquires the second mutex.
- The first worker waits for the second mutex.
- The second worker waits for the first mutex.
- Neither worker can reach its unlock operations.
- The main thread waits indefinitely while joining the workers.
Prevent Deadlock with Lock Ordering
Define one global order for lock acquisition and require every code path to follow it.
Approved order:
1. Account mutex
2. Ledger mutex
3. Audit mutex
Every operation acquires locks only
from lower order to higher order.
Corrected Pattern
static int lock_both(
pthread_mutex_t *first,
pthread_mutex_t *second)
{
if (pthread_mutex_lock(
first) != 0) {
return 0;
}
if (pthread_mutex_lock(
second) != 0) {
pthread_mutex_unlock(
first
);
return 0;
}
return 1;
}
static void unlock_both(
pthread_mutex_t *first,
pthread_mutex_t *second)
{
pthread_mutex_unlock(
second
);
pthread_mutex_unlock(
first
);
}
Every caller must pass the locks in the approved order. A stronger design can encode resource ranking so callers do not choose the order manually.
Consistent Worker Pattern
static void *worker(
void *argument)
{
(void)argument;
if (!lock_both(
&first_mutex,
&second_mutex)) {
return NULL;
}
/*
* Protected operation using
* both related resources.
*/
unlock_both(
&first_mutex,
&second_mutex
);
return NULL;
}
Circular wait cannot form between these two mutexes when every participating path follows the same order.
Resource Hierarchy
A resource hierarchy assigns every lock or resource class a rank.
| Rank | Resource |
|---|---|
| 10 | Customer lock |
| 20 | Order lock |
| 30 | Payment lock |
| 40 | Audit lock |
Valid acquisition:
Customer -> Order -> Payment -> Audit
Invalid acquisition:
Payment -> Customer
Lock-order documentation should be maintained near synchronization interfaces and validated through code review or diagnostic assertions where practical.
Dynamic Resource Ordering
Sometimes a program must lock several resources of the same type, such as two bank accounts. Use a stable key to determine the order.
static int lock_accounts(
struct Account *first,
struct Account *second)
{
if (first == NULL ||
second == NULL ||
first == second) {
return 0;
}
struct Account *lower =
first->id < second->id
? first
: second;
struct Account *higher =
first->id < second->id
? second
: first;
if (pthread_mutex_lock(
&lower->mutex) != 0) {
return 0;
}
if (pthread_mutex_lock(
&higher->mutex) != 0) {
pthread_mutex_unlock(
&lower->mutex
);
return 0;
}
return 1;
}
The corresponding unlock operation should follow the documented reverse order. The design must handle the case where both arguments refer to the same account according to the operation contract.
Bank Transfer Example
A transfer between two accounts frequently requires coordinated updates to both account records.
pthread_mutex_lock(
&source->mutex
);
pthread_mutex_lock(
&destination->mutex
);
A simultaneous transfer in the opposite direction can reverse the acquisition order.
Transfer 1:
Lock Account A
Wait for Account B
Transfer 2:
Lock Account B
Wait for Account A
struct Account *lower =
source->id < destination->id
? source
: destination;
struct Account *higher =
source->id < destination->id
? destination
: source;
pthread_mutex_lock(
&lower->mutex
);
pthread_mutex_lock(
&higher->mutex
);
/*
* Validate and apply the complete
* transfer while both accounts
* are protected.
*/
pthread_mutex_unlock(
&higher->mutex
);
pthread_mutex_unlock(
&lower->mutex
);
Reduce Simultaneous Lock Ownership
Deadlock risk can be reduced when operations do not hold several resources simultaneously.
lock(
&cache_mutex
);
lock(
&database_mutex
);
update_cache_and_database();
unlock(
&database_mutex
);
unlock(
&cache_mutex
);
struct Record snapshot;
lock(
&database_mutex
);
snapshot =
update_database();
unlock(
&database_mutex
);
lock(
&cache_mutex
);
update_cache(
&snapshot
);
unlock(
&cache_mutex
);
Separating the operations is valid only when temporary divergence and failure between phases are handled according to the consistency contract.
Timed Lock Acquisition
A timed lock attempt prevents one participant from waiting indefinitely. It can detect a possible progress problem, but it does not automatically make the overall operation correct.
Attempt to acquire Lock B
|
+--> Acquired: continue
|
+--> Deadline reached:
release held resources
return controlled failure
retry only according to policy
POSIX Timed-lock Pattern
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <pthread.h>
#include <time.h>
static int add_milliseconds(
struct timespec *deadline,
long milliseconds)
{
if (deadline == NULL ||
milliseconds < 0) {
return 0;
}
deadline->tv_sec +=
milliseconds / 1000;
deadline->tv_nsec +=
(milliseconds % 1000)
* 1000000L;
if (deadline->tv_nsec >=
1000000000L) {
++deadline->tv_sec;
deadline->tv_nsec -=
1000000000L;
}
return 1;
}
static int lock_with_timeout(
pthread_mutex_t *mutex,
long timeout_milliseconds)
{
if (mutex == NULL) {
return 0;
}
struct timespec deadline;
if (clock_gettime(
CLOCK_REALTIME,
&deadline) != 0 ||
!add_milliseconds(
&deadline,
timeout_milliseconds)) {
return 0;
}
int status =
pthread_mutex_timedlock(
mutex,
&deadline
);
if (status == 0) {
return 1;
}
if (status == ETIMEDOUT) {
return 0;
}
return 0;
}
Availability of timed mutex operations and clock behaviour depend on the target POSIX implementation. A timeout can also occur because of ordinary contention, not only deadlock.
Try-lock and Retry
A try-lock operation attempts acquisition without indefinite blocking.
Acquire Lock A
|
v
Try Lock B
|
+--> Success:
| complete operation
|
+--> Busy:
release Lock A
wait according to policy
retry
This can break hold-and-wait, but careless retry loops can create livelock or high CPU consumption.
for (;;) {
if (try_operation()) {
break;
}
}
Maximum attempts:
Defined by policy
Delay:
Backoff with controlled randomness
Deadline:
Stop after the operation deadline
Failure:
Return a controlled retryable result
Observability:
Record contention and retry exhaustion
Livelock
Livelock occurs when participants remain active and repeatedly change their behaviour in response to one another, but useful work does not complete.
Thread A detects conflict and releases.
Thread B detects conflict and releases.
Thread A retries immediately.
Thread B retries immediately.
Both conflict again.
The cycle repeats.
Possible controls include:
- Randomized backoff
- Priority rules
- Bounded retries
- Single ownership
- Queue-based serialization
Starvation
Starvation occurs when one participant repeatedly fails to obtain the resources needed to progress, even though the system as a whole continues processing other work.
Thread A repeatedly acquires the lock.
Thread B waits.
Thread C repeatedly acquires the lock.
Thread B continues waiting.
Starvation can result from:
- Unfair resource admission
- Reader-preferred locking
- High-priority work arriving continuously
- Unbounded retries by competing tasks
- Resource limits that never favor an older waiter
Wait-for Graph
A wait-for graph represents participants and waiting dependencies.
Thread A ---> Thread B
^ |
| v
Thread D <--- Thread C
An arrow from Thread A to Thread B means Thread A waits for a resource held by Thread B.
A cycle in a single-instance resource wait-for graph indicates a deadlock. More complex resource models can require additional analysis.
Deadlock Detection
Detection allows deadlocks to occur and then identifies blocked dependency cycles.
A detection system can use:
- Wait-for graph analysis
- Database deadlock detection
- Thread dumps
- Lock-owner diagnostics
- Watchdogs
- Timeout monitoring
- Stalled-progress metrics
A timeout can identify unusually long waiting, but long waiting alone does not prove a deadlock.
Deadlock Recovery
Recovery breaks a detected cycle by forcing one or more participants to release resources or terminate.
Recovery options include:
- Abort one transaction
- Roll back one operation
- Terminate and restart one worker
- Cancel selected requests
- Revoke a lease when its protocol permits expiry
- Restart the affected process as a last-resort operational action
A recovery policy should define:
- How the victim is selected
- Which work can be safely rolled back
- Whether retries are safe
- How partial external effects are reconciled
- How repeated deadlocks are detected and escalated
Prevention vs Avoidance vs Detection
| Strategy | Approach | Main Trade-off |
|---|---|---|
| Prevention | Structurally break at least one necessary condition | Can reduce flexibility or concurrency |
| Avoidance | Grant resources only when the resulting state remains safe | Requires advance resource information and runtime analysis |
| Detection | Allow waiting and inspect for dependency cycles | Requires recovery after deadlock occurs |
| Timeout-based control | Stop waiting after a deadline | Can report ordinary contention as failure |
Database Deadlocks
Database transactions can deadlock when they acquire row, page, table or other database locks in conflicting orders.
Transaction A:
Lock Row 1
Wait for Row 2
Transaction B:
Lock Row 2
Wait for Row 1
A database engine can detect the cycle and abort one transaction so the other can continue.
Application responsibilities include:
- Keeping transactions focused
- Accessing records in a consistent order
- Avoiding unnecessary user or network waiting inside transactions
- Handling deadlock-victim errors
- Retrying only when the operation is safe
- Applying a bounded retry policy
- Recording repeated deadlock patterns
Consistent Record Order
Unsafe:
Transaction A updates Customer 10, then Customer 20.
Transaction B updates Customer 20, then Customer 10.
Safer ordering:
Every transaction updates lower customer ID first,
then higher customer ID.
Thread-pool Deadlock
Deadlocks can occur without explicit mutex cycles. A task can wait for another task that cannot run because all worker threads are occupied.
Thread pool capacity: 2
Worker 1:
Runs Task A.
Task A submits Task C.
Task A waits for Task C.
Worker 2:
Runs Task B.
Task B submits Task D.
Task B waits for Task D.
Task C and Task D:
Queued, but no worker is available.
Possible design corrections include:
- Do not wait synchronously for work submitted to the same saturated pool.
- Use asynchronous composition.
- Separate dependent workloads into appropriate executors.
- Execute small child tasks directly when the design permits.
- Use bounded waiting and overload controls.
Connection-pool Deadlock
A resource pool can participate in deadlock when operations retain one resource while waiting for another resource from a fully utilized pool.
Pool contains two connections.
Thread A holds Connection 1.
Thread A waits for another connection.
Thread B holds Connection 2.
Thread B waits for another connection.
No connection can become free.
The operation should avoid requesting more pooled resources than its contract allows while retaining existing resources.
Distributed Deadlocks
A distributed deadlock can form across service or machine boundaries.
Service A holds Resource A
and synchronously calls Service B.
Service B holds Resource B
and synchronously calls Service A.
Both calls wait for the other service.
Distributed deadlocks are harder to diagnose because:
- Wait relationships cross process and machine boundaries.
- There may be no single global lock manager.
- Network failure and delay can resemble blocking.
- Timeouts can trigger retries and duplicate work.
- Partial external effects may not be automatically reversible.
Useful controls include:
- Request deadlines
- Acyclic service-call design
- Asynchronous workflows
- Single ownership of state
- Transactional coordination where required
- Tracing of cross-service dependencies
Nested Callback Deadlock
A component can deadlock when it calls external or reentrant code while holding a lock.
Component A acquires Lock A.
Component A invokes callback in Component B.
Component B invokes Component A again.
Component A tries to acquire Lock A.
The same logical operation blocks itself.
Avoid invoking unknown callbacks, extension code, event handlers or remote services while holding internal locks unless the contract explicitly supports reentrancy.
Symptoms of Deadlock
Common symptoms include:
- Requests never complete
- Worker utilization appears low while queues increase
- Several threads remain blocked on locks
- Application shutdown never finishes
- Thread joins wait indefinitely
- No progress occurs despite an active process
- Database transactions are selected as deadlock victims
- Watchdogs report stalled work
- Connection or thread pools remain exhausted
These symptoms can also result from remote hangs, slow I/O or starvation. Diagnosis must inspect the actual waiting dependencies.
Linux Deadlock Investigation
Inspect Threads
ps -T -p PROCESS_ID
Observe Thread States
top -H -p PROCESS_ID
Inspect Process Status
cat /proc/PROCESS_ID/status
Attach a Debugger
gdb -p PROCESS_ID
Inside an applicable GDB session:
info threads
thread apply all backtrace
Thread backtraces can show several threads blocked inside lock-acquisition calls and help identify the resource cycle.
Trace System Calls
strace -f -p PROCESS_ID
Tool availability, output and permissions vary by Linux environment. Attaching diagnostic tools can affect timing and performance, so follow the approved operational process.
Deadlock Investigation Process
- Identify the user-visible operation that stopped progressing.
- Capture thread dumps before restarting the process.
- Identify threads blocked on synchronization or resource acquisition.
- Determine which participant owns each requested resource.
- Construct a wait-for graph.
- Check for a dependency cycle.
- Identify which necessary conditions were allowed.
- Choose a prevention, avoidance, detection or recovery strategy.
- Add a regression test that recreates the hazardous ordering.
- Measure contention after applying the correction.
Deadlock Testing
Deadlock testing should deliberately create overlapping resource acquisition rather than waiting for a rare production ordering.
| Test | Expected Evidence |
|---|---|
| Opposite resource requests | All operations follow one global acquisition order |
| Simultaneous thread start | Workers begin together to maximize lock overlap |
| High-contention run | Every task eventually completes or returns a controlled result |
| Timed acquisition | Deadline expiry releases already held resources safely |
| Connection-pool exhaustion | Operations do not hold one connection while waiting indefinitely for another |
| Thread-pool saturation | Tasks do not synchronously wait for queued work that cannot run |
| Database deadlock victim | The application rolls back and applies the bounded retry policy |
| Concurrent shutdown | Waiting threads wake and release resources correctly |
| Repeated stress test | No permanent progress failure occurs across repeated runs |
Deadlock Review Checklist
Review Every Multi-resource Operation
- Every synchronization resource has a documented purpose.
- Operations requiring several resources are identified.
- A global resource-acquisition order is documented.
- Every code path follows the same order.
- Dynamic resources use a stable ordering key.
- Locks are released in a documented sequence.
- Error paths release every successfully acquired resource.
- Slow network and disk operations are not performed while holding unrelated locks.
- Unknown callbacks are not invoked while holding internal locks.
- Condition-variable waits release the associated mutex correctly.
- Thread-pool tasks do not wait for work queued to the same exhausted pool.
- Connection usage does not exceed the per-operation contract.
- Database records are accessed in a consistent order.
- Database deadlock-victim errors have a safe policy.
- Timed waits distinguish contention from permanent failure.
- Retries are bounded and cannot create livelock.
- Shutdown wakes blocked participants.
- Thread dumps and wait relationships can be captured.
- Tests verify progress, not only final data correctness.
- Contention introduced by prevention is measured.
Common Deadlock Mistakes
Acquiring the Same Locks in Different Orders
Opposite acquisition orders can create a circular wait.
Calling External Code While Holding a Lock
External code can block, call back into the component or acquire additional resources in an unknown order.
Holding Locks During Slow I/O
Disk and network delays extend lock ownership and increase blocking and dependency cycles.
Forgetting an Error-path Unlock
Every successful acquisition requires a release path, including failures and early returns.
Using Timeouts as the Only Design
A timeout limits waiting but does not explain how partial work, held resources and retries remain correct.
Retrying Immediately After Conflict
Identical immediate retries can create livelock or sustained contention.
Ignoring Thread-pool Dependency Cycles
Tasks can deadlock when all workers wait for child work queued to the same exhausted pool.
Ignoring Database Lock Order
Transactions updating the same records in different orders can deadlock even when application memory is correctly synchronized.
Confusing Long Waiting with Deadlock
Diagnose the resource dependency cycle rather than assuming every slow lock acquisition is a deadlock.
Collecting Diagnostics Only After Restart
Capture thread states and resource ownership while the progress failure is still present when operational policy permits it.
Fixing Deadlock by Removing Required Synchronization
Eliminating a lock without preserving the shared invariant can replace a deadlock with a race condition or corrupted state.
Using Too Many Fine-grained Locks Prematurely
Additional locks increase ordering complexity. Begin with a simple correct model and refine it only after measuring contention.
Deadlock Prevention Best Practices
Recommended Practices
- Minimize the number of simultaneously held resources.
- Define and document one global lock order.
- Use stable identifiers to order dynamic resources.
- Keep critical sections focused and bounded.
- Do not call unknown code while holding internal locks.
- Avoid waiting for network or disk operations while holding locks.
- Release partially acquired resources on every failure path.
- Use bounded waiting when indefinite blocking is unacceptable.
- Apply backoff and retry limits to conflict recovery.
- Design thread pools and connection pools with dependency behaviour in mind.
- Keep database transactions focused.
- Update database resources in a consistent order.
- Handle database deadlock-victim errors explicitly.
- Prevent cyclic synchronous service calls.
- Use queues or single ownership when sequential state mutation is simpler.
- Include deadlock behaviour in shutdown design.
- Capture thread dumps and wait relationships during investigation.
- Test progress under contention and failure.
Practice Exercise
Analyze and correct a deadlock in a two-account transfer operation.
Defective Logic
static int transfer(
struct Account *source,
struct Account *destination,
long long amount)
{
pthread_mutex_lock(
&source->mutex
);
pthread_mutex_lock(
&destination->mutex
);
if (amount > 0 &&
source->balance >=
amount) {
source->balance -=
amount;
destination->balance +=
amount;
}
pthread_mutex_unlock(
&destination->mutex
);
pthread_mutex_unlock(
&source->mutex
);
return 1;
}
Tasks
- Identify the two exclusive resources.
- Write a circular-wait scenario involving opposite transfers.
- Define a stable account-lock ordering rule.
- Handle transfers where source and destination are the same account.
- Validate the amount before changing balances.
- Check arithmetic overflow before increasing the destination balance.
- Release partially acquired resources when acquisition fails.
- Write a concurrent transfer stress test.
- Verify that total money remains unchanged.
- Verify that every test completes within the defined deadline.
Model Lock-order Correction
static int transfer(
struct Account *source,
struct Account *destination,
long long amount)
{
if (source == NULL ||
destination == NULL ||
source == destination ||
amount <= 0) {
return 0;
}
struct Account *lower =
source->id <
destination->id
? source
: destination;
struct Account *higher =
source->id <
destination->id
? destination
: source;
if (pthread_mutex_lock(
&lower->mutex) != 0) {
return 0;
}
if (pthread_mutex_lock(
&higher->mutex) != 0) {
pthread_mutex_unlock(
&lower->mutex
);
return 0;
}
int succeeded = 0;
if (source->balance >=
amount) {
source->balance -=
amount;
destination->balance +=
amount;
succeeded = 1;
}
pthread_mutex_unlock(
&higher->mutex
);
pthread_mutex_unlock(
&lower->mutex
);
return succeeded;
}
A complete financial implementation must additionally check destination overflow, define monetary representation and preserve transactional and audit requirements. This example focuses on lock ordering.
Transfer Invariant
For a successful transfer:
source balance decreases by amount
destination balance increases by amount
Combined balance remains unchanged
No account balance becomes invalid
Both account updates are observed as one
protected logical operation
Frequently Asked Questions
What is a deadlock?
A deadlock is a state in which participants cannot progress because each waits for a resource or action controlled by another blocked participant.
What are the four necessary deadlock conditions?
The four conditions are mutual exclusion, hold and wait, no preemption and circular wait.
What is the most practical lock-based prevention technique?
A consistent global lock-acquisition order is commonly used to prevent circular wait.
Is every long lock wait a deadlock?
No. Long contention can eventually resolve. A deadlock involves a dependency cycle that prevents the affected participants from progressing.
What is the difference between deadlock and starvation?
In deadlock, participants block one another through a dependency cycle. In starvation, one participant repeatedly fails to obtain resources while other work continues.
What is the difference between deadlock and livelock?
Deadlocked participants remain blocked. Livelocked participants remain active but repeatedly react without completing useful work.
Do timeouts prevent deadlocks?
Timeouts prevent indefinite waiting in selected paths, but the program must still release resources and handle partial work correctly.
Can one mutex cause a deadlock?
Yes. A non-recursive mutex can participate in self-deadlock when the same thread tries to acquire it again before releasing it.
Can databases experience deadlocks?
Yes. Transactions can acquire database locks in conflicting orders. The database can detect the cycle and select one transaction for rollback.
Can a thread pool deadlock?
Yes. All workers can block while waiting for child work queued to the same exhausted pool.
Can deadlocks occur in distributed systems?
Yes. Services, transactions or workers can form waiting cycles across process and machine boundaries.
What comes after deadlocks?
The next topic is files and sockets, followed by Linux process and network tools.
Key Takeaway
Deadlock occurs when participants form a permanent waiting cycle. Classic deadlock requires mutual exclusion, hold and wait, no preemption and circular wait. Prevent circular wait through consistent resource ordering, minimize simultaneous ownership, avoid unknown or slow work while holding locks and design bounded waiting and recovery explicitly. Diagnose suspected deadlocks by capturing thread states, mapping resource ownership and constructing the wait-for dependency cycle.