# Lecture 14 - Deadlock

Source: https://wisconsin.twango.dev/sp26-cs537/lectures/lecture-14

# Lecture 14 - Deadlock

**Instructor:** Mike Swift & Tej Chajed
**Topics:** Deadlock, four conditions for deadlock, deadlock prevention, lock ordering, lock-free programming

> [!abstract]- 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:

```c
// Thread 1:              // Thread 2:
lock(&A);                 lock(&B);
lock(&B);                 lock(&A);
// ...                    // ...
unlock(A);                unlock(A);
unlock(B);                unlock(B);
```

> [!note] 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:

```mermaid
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**:

```c
// 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:

```c
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);
}
```

> [!note] Lecture Note
> The loop body (checking `s2` and adding to `rv`) is the critical section that requires both locks.

The deadlock arises when two threads call the function with **swapped arguments**:

```c
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:

```c
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);
}
```

> [!note] 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:

```c
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:

1. **Circular wait** — a circular chain of threads, each holding a resource needed by the next
2. **Hold-and-wait** — threads hold resources while waiting for additional resources
3. **No preemption** — resources cannot be forcibly taken from threads holding them
4. **Mutual exclusion** — threads claim exclusive control of resources

> [!tip] 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

> [!note] 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**:

```c
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.

```c
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:

```c
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

```c
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:**

```c
void insert(int val) {
    node_t *n = malloc(...);
    n->val = val;
    lock(&m);
    n->next = head;
    head = n;
    unlock(&m);
}
```

> [!note] Lecture Note
> The `malloc` and value assignment are outside the critical section. Only the pointer manipulation (setting `n->next` and updating `head`) needs protection.

**Lock-free version:**

```c
void insert(int val) {
    node_t *n = malloc(...);
    n->val = val;
    do {
        n->next = head;
    } while (!CompareAndSwap(&head, n->next, n));
}
```

> [!note] Lecture Note
> `CompareAndSwap(&head, n->next, n)` atomically checks if `head` still equals the stored address (`n->next` is the expected value). If another thread modified `head` in the meantime, the CAS fails and the loop retries with the updated head value.

---

## Related

- **Navigation:** [[lecture-13|← Lecture 13]] · [[lecture-15|Lecture 15 →]]
- **Exam:** [[exams/midterm-2/index|Midterm 2]]
- **Course:** [[README|CS 537 Map of Content]]
