# 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]]