Lecture 14 - Deadlock
Instructor: Mike Swift & Tej Chajed Topics: Deadlock, four conditions for deadlock, deadlock prevention, lock ordering, lock-free programming
Learning Outcomes
- Describe what deadlocks are and why they occur
- Recall how to prevent deadlocks
What is Deadlock?
Deadlock: A set of threads is blocked indefinitely because each thread is waiting for a resource another thread holds.
No progress can be made because two or more threads are waiting for the other to take some action and thus neither ever does.
Real-world analogy: Gridlock at an intersection — cars from all four directions block each other and none can proceed.
Deadlock vs Starvation vs Livelock
Deadlock: A set of threads is blocked indefinitely because each thread is waiting for a resource another thread holds.
Starvation: A thread is blocked indefinitely because it is waiting for resource(s) held by other thread(s). (Differs from deadlock: the other threads are not necessarily waiting for the starved thread.)
Livelock: A set of threads are active but make no progress. (Threads keep running and changing state, but no useful work gets done.)
Deadlock Code Example
Consider two threads acquiring two locks in opposite order:
// Thread 1: // Thread 2:
lock(&A); lock(&B);
lock(&B); lock(&A);
// ... // ...
unlock(A); unlock(A);
unlock(B); unlock(B);
Lecture Note
If Thread 1 holds lock A and waits for lock B, while Thread 2 holds lock B and waits for lock A, neither can proceed — this is deadlock.
Circular Dependency
This creates a circular dependency between threads and locks:
graph LR
T1((Thread 1)) -->|holds| LA[Lock A]
LA -->|wanted by| T2((Thread 2))
T2 -->|holds| LB[Lock B]
LB -->|wanted by| T1
Fixing the Deadlock
Ensure both threads acquire locks in the same order:
// Thread 1: // Thread 2:
lock(&A); lock(&A);
lock(&B); lock(&B);
This breaks the circular dependency — both threads try to acquire A first, so one will succeed and the other will wait without holding any conflicting lock.
Set Intersection Example
A more realistic example where deadlock hides inside a function:
set_t *set_intersection(set_t *s1, set_t *s2) {
set_t *rv = malloc(sizeof(*rv));
mutex_lock(&s1->lock);
mutex_lock(&s2->lock);
for (int i = 0; i < s1->len; i++) {
if (set_contains(s2, s1->items[i]))
set_add(rv, s1->items[i]);
}
mutex_unlock(&s2->lock);
mutex_unlock(&s1->lock);
}
Lecture Note
The loop body (checking
s2and adding torv) is the critical section that requires both locks.
The deadlock arises when two threads call the function with swapped arguments:
Thread 1: rv = set_intersection(setA, setB); // locks A then B
Thread 2: rv = set_intersection(setB, setA); // locks B then A
Modularity / Encapsulation
Modularity can make it harder to see deadlocks — the lock ordering is hidden inside a function.
Solution: Compare lock addresses to enforce a consistent ordering:
if (m1 > m2) {
// grab locks in high-to-low address order
pthread_mutex_lock(m1);
pthread_mutex_lock(m2);
} else {
pthread_mutex_lock(m2);
pthread_mutex_lock(m1);
}
Lecture Note
If
m1 == m2, this causes immediate deadlock (locking the same mutex twice). Must handle that case separately.
Practice: Which Can Deadlock?
Given a function that acquires three locks in order:
void foo(pthread_mutex_t *t1, pthread_mutex_t *t2, pthread_mutex_t *t3) {
pthread_mutex_lock(t1);
pthread_mutex_lock(t2);
pthread_mutex_lock(t3);
do_stuffs();
pthread_mutex_unlock(t1);
pthread_mutex_unlock(t2);
pthread_mutex_unlock(t3);
}
| Scenario | Thread calls | Can deadlock? |
|---|---|---|
| 1 | T1 foo(a,b,c), T2 foo(b,c,a), T3 foo(c,a,b) | Yes — circular ordering (a→b→c→a) |
| 2 | T1 foo(a,b,c), T2 foo(a,b,c), T3 foo(a,b,c) | No — all same order |
| 3 | T1 foo(a,b,c), T2 foo(b,c,e), T3 foo(f,e,a) | No — no circular dependency among shared locks |
Four Conditions for Deadlock (Deadlock Theory)
Deadlocks can only happen when all four of these conditions hold simultaneously:
- Circular wait — a circular chain of threads, each holding a resource needed by the next
- Hold-and-wait — threads hold resources while waiting for additional resources
- No preemption — resources cannot be forcibly taken from threads holding them
- Mutual exclusion — threads claim exclusive control of resources
Key Insight
Eliminating any one condition prevents deadlock. Each prevention strategy targets one of these four conditions.
Prevention Strategy 1: Lock Ordering (Eliminates Circular Wait)
Problem: Circular chain of threads such that each thread holds a resource (e.g., lock) being requested by the next thread in the chain.
Strategy: Decide on a lock ordering and always acquire locks in that order.
- If A comes before B in the ordering, never acquire A if B is already held
- Document the ordering and write code accordingly
Lecture Note
Works well if the system has distinct layers. The ordering can follow the layer hierarchy (e.g., filesystem lock before buffer cache lock before disk lock).
Prevention Strategy 2: Avoid Waiting While Holding Lock (Eliminates Hold-and-Wait)
Problem: Threads hold resources allocated to them while waiting for additional resources.
Strategy: Acquire all locks atomically once. Can release locks over time, but cannot acquire again until all have been released.
Use a meta lock:
lock(&meta);
lock(&L1);
lock(&L2);
// ...
unlock(&meta);
// Critical section code
unlock(/* ... */);
Disadvantages:
- Must know ahead of time which locks will be needed
- Must be conservative (acquire any lock possibly needed)
- If too many locks are acquired, degenerates to just having one big lock
Prevention Strategy 3: Preempt Lock Owners (Eliminates No-Preemption)
Problem: Resources (e.g., locks) cannot be forcibly removed from threads that are holding them.
Strategy: If a thread can’t get what it wants, release what it holds and retry.
top:
lock(A);
if (trylock(B) == -1) {
unlock(A);
goto top;
}
// ...
Disadvantage: This can cause livelock — threads keep retrying but make no progress because they keep releasing and re-acquiring in lockstep.
Classic solution: Exponential back-off — add increasing random delays between retries:
delay = 1;
top:
lock(A);
if (trylock(B) == -1) {
unlock(A);
sleep(delay); delay *= 2;
goto top;
}
// ...
Prevention Strategy 4: Write Lock-Free Code (Eliminates Mutual Exclusion)
Problem: Threads claim exclusive control of resources that they require.
Strategy: Eliminate locks! Replace locks with atomic primitives.
CompareAndSwap
int CompareAndSwap(int *address, int expected, int new) {
if (*address == expected) {
*address = new;
return 1; // success
}
return 0; // failure
}
Lock-Free Linked List Insert
With locks:
void insert(int val) {
node_t *n = malloc(...);
n->val = val;
lock(&m);
n->next = head;
head = n;
unlock(&m);
}
Lecture Note
The
mallocand value assignment are outside the critical section. Only the pointer manipulation (settingn->nextand updatinghead) needs protection.
Lock-free version:
void insert(int val) {
node_t *n = malloc(...);
n->val = val;
do {
n->next = head;
} while (!CompareAndSwap(&head, n->next, n));
}
Lecture Note
CompareAndSwap(&head, n->next, n)atomically checks ifheadstill equals the stored address (n->nextis the expected value). If another thread modifiedheadin the meantime, the CAS fails and the loop retries with the updated head value.
Related
- Navigation: ← Lecture 13 · Lecture 15 →
- Exam: Midterm 2
- Course: CS 537 Map of Content