30.1.4 — Mutex Deadlocks

 

← Chapter 30 Index

30.1.4 — Mutex Deadlocks

When threads wait for each other forever — causes, detection, and prevention

Key Terms in This File

deadlock circular wait lock hierarchy lock ordering try-and-back-off starvation

What Is a Deadlock?

A deadlock is a situation where two or more threads are permanently blocked, each waiting for a resource that another blocked thread holds. None of them can ever proceed.

Think of it like two people each holding one end of a rope, each waiting for the other to let go first. Neither will ever move.

In multi-threaded programs, deadlock usually involves multiple mutexes. It does NOT require a mistake in logic — it can happen with correct-looking code if threads acquire mutexes in different orders.

Classic Two-Mutex Deadlock

Here is the most common deadlock scenario: Thread A locks mutex1 then tries to lock mutex2. Thread B locks mutex2 then tries to lock mutex1.

Deadlock Scenario (Figure 30-3 from TLPI)

Step Thread A Thread B State
1 pthread_mutex_lock(mutex1) ✓ A holds mutex1
2 pthread_mutex_lock(mutex2) ✓ B holds mutex2
3 pthread_mutex_lock(mutex2) ⏸ BLOCKS A waiting for mutex2 (held by B)
4 pthread_mutex_lock(mutex1) ⏸ BLOCKS B waiting for mutex1 (held by A)
💀 DEADLOCK — Both threads blocked forever. Program hangs.
Deadlock is a silent killer. The program does not crash — it simply hangs forever. No error message. No signal. The process uses no CPU but never terminates. This can be extremely hard to diagnose in production systems.

Code Example 1: Deadlock in Action

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

/* Thread A: locks mutex1 FIRST, then mutex2 */
void *threadA(void *arg)
{
    printf("Thread A: locking mutex1\n");
    pthread_mutex_lock(&mutex1);
    printf("Thread A: locked mutex1, now trying mutex2\n");

    sleep(1); /* Give Thread B time to lock mutex2 */

    printf("Thread A: trying to lock mutex2 (will DEADLOCK)\n");
    pthread_mutex_lock(&mutex2);  /* <-- Will block here FOREVER */

    printf("Thread A: locked both (will never print)\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}

/* Thread B: locks mutex2 FIRST, then mutex1 (opposite order!) */
void *threadB(void *arg)
{
    printf("Thread B: locking mutex2\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread B: locked mutex2, now trying mutex1\n");

    sleep(1); /* Give Thread A time to lock mutex1 */

    printf("Thread B: trying to lock mutex1 (will DEADLOCK)\n");
    pthread_mutex_lock(&mutex1);  /* <-- Will block here FOREVER */

    printf("Thread B: locked both (will never print)\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}

int main(void)
{
    pthread_t tA, tB;
    pthread_create(&tA, NULL, threadA, NULL);
    pthread_create(&tB, NULL, threadB, NULL);

    pthread_join(tA, NULL);  /* Will wait forever */
    pthread_join(tB, NULL);  /* Will wait forever */

    printf("Done (will never print)\n");
    return 0;
}

/*
 * Compile: gcc -o deadlock deadlock.c -lpthread
 * Run: ./deadlock
 * The program will hang — press Ctrl+C to kill it.
 *
 * Output before hanging:
 *   Thread A: locking mutex1
 *   Thread B: locking mutex2
 *   Thread A: locked mutex1, now trying mutex2
 *   Thread B: locked mutex2, now trying mutex1
 *   Thread A: trying to lock mutex2 (will DEADLOCK)
 *   Thread B: trying to lock mutex1 (will DEADLOCK)
 *   [Program hangs here]
 */

Solution 1: Lock Hierarchy (Recommended)

The simplest and most reliable way to prevent deadlock is the mutex hierarchy rule:

Rule: When any thread needs to acquire multiple mutexes simultaneously, all threads must always lock them in the same global order. If Thread A always locks mutex1 before mutex2, then Thread B must also always lock mutex1 before mutex2 — never the other way around.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

/* FIXED Thread A: locks mutex1 FIRST, then mutex2 */
void *threadA_fixed(void *arg)
{
    pthread_mutex_lock(&mutex1);   /* Always lock mutex1 first */
    printf("Thread A: locked mutex1\n");
    sleep(1);
    pthread_mutex_lock(&mutex2);   /* Then mutex2 */
    printf("Thread A: locked mutex2\n");

    /* Critical work */
    printf("Thread A: doing work with both mutexes\n");

    pthread_mutex_unlock(&mutex2); /* Unlock in reverse order */
    pthread_mutex_unlock(&mutex1);
    printf("Thread A: done\n");
    return NULL;
}

/* FIXED Thread B: ALSO locks mutex1 FIRST, then mutex2 (same order!) */
void *threadB_fixed(void *arg)
{
    pthread_mutex_lock(&mutex1);   /* Same order as Thread A */
    printf("Thread B: locked mutex1\n");
    sleep(1);
    pthread_mutex_lock(&mutex2);
    printf("Thread B: locked mutex2\n");

    printf("Thread B: doing work with both mutexes\n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    printf("Thread B: done\n");
    return NULL;
}

int main(void)
{
    pthread_t tA, tB;
    pthread_create(&tA, NULL, threadA_fixed, NULL);
    pthread_create(&tB, NULL, threadB_fixed, NULL);
    pthread_join(tA, NULL);
    pthread_join(tB, NULL);
    printf("Both threads completed successfully — no deadlock!\n");
    return 0;
}

/*
 * One thread will block waiting for mutex1 while the other
 * holds it and acquires mutex2. But since BOTH want mutex1 first,
 * there is no circular wait. Eventually Thread A gets both, finishes,
 * and releases — then Thread B gets both. No deadlock.
 */

Solution 2: Try-and-Back-Off

An alternative strategy: lock the first mutex with pthread_mutex_lock(), then try the remaining ones with pthread_mutex_trylock(). If any trylock fails, release all locks and retry after a delay.

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

/* Try to acquire both mutexes; back off and retry if we can't */
int acquire_both(pthread_mutex_t *m1, pthread_mutex_t *m2)
{
    while (1) {
        /* Lock the first one (blocking is OK) */
        pthread_mutex_lock(m1);

        /* Try the second without blocking */
        if (pthread_mutex_trylock(m2) == 0) {
            return 1;  /* Got both! */
        }

        /* Couldn't get m2 — release m1 and back off */
        pthread_mutex_unlock(m1);

        /* Small random delay to reduce livelock */
        struct timespec ts = {0, (rand() % 10 + 1) * 1000000};
        nanosleep(&ts, NULL);
        printf("Backing off, retrying...\n");
    }
}

void *threadA(void *arg)
{
    acquire_both(&mutex1, &mutex2);
    printf("Thread A: has both locks\n");
    /* do work */
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}

void *threadB(void *arg)
{
    /* Thread B tries in a different order — would cause deadlock
     * with straight locking, but try-and-backoff prevents it */
    acquire_both(&mutex2, &mutex1);
    printf("Thread B: has both locks\n");
    /* do work */
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}

int main(void)
{
    pthread_t tA, tB;
    pthread_create(&tA, NULL, threadA, NULL);
    pthread_create(&tB, NULL, threadB, NULL);
    pthread_join(tA, NULL);
    pthread_join(tB, NULL);
    printf("Both done — no deadlock\n");
    return 0;
}
Try-and-back-off vs. Lock hierarchy: Lock hierarchy is simpler, more efficient, and should be your first choice. Try-and-back-off is more flexible (no requirement for a fixed order) but less efficient — it may retry many times. Use it when a strict lock order is logically difficult or impossible to impose.

Interview Questions — Mutex Deadlocks

Q1. What is a deadlock? When does it occur with mutexes? A deadlock is a permanent blocking situation where two or more threads are each waiting for a mutex held by another blocked thread. With mutexes, it typically occurs when Thread A holds mutex1 and waits for mutex2, while Thread B holds mutex2 and waits for mutex1 — creating a circular wait.
Q2. What are the four necessary conditions for deadlock? (1) Mutual exclusion — resources cannot be shared. (2) Hold and wait — a thread holds one resource while waiting for another. (3) No preemption — resources cannot be forcibly taken away. (4) Circular wait — a circular chain of threads, each waiting for a resource held by the next. All four must be present for deadlock.
Q3. How does lock hierarchy prevent deadlock? By requiring all threads to acquire multiple mutexes in the same global order, the circular wait condition is broken. If all threads always lock mutex1 before mutex2, no thread holding mutex2 will ever be waiting for mutex1 — so a deadlock cycle cannot form.
Q4. What is the “try-and-back-off” strategy for deadlock avoidance? A thread acquires the first mutex with pthread_mutex_lock(), then tries remaining mutexes with pthread_mutex_trylock(). If any trylock fails (returns EBUSY), the thread releases all held mutexes and retries after a delay. This avoids deadlock at the cost of efficiency (multiple retries may be needed).
Q5. How does a deadlocked program behave? Can you detect it? A deadlocked program hangs silently — it does not crash, does not print errors, but uses no CPU and never terminates. You can detect it with tools like: gdb (attach and check thread stacks), pstack, strace, or thread sanitizers (like ThreadSanitizer / TSAN with GCC/Clang).
Q6. Which deadlock strategy is preferred: lock hierarchy or try-and-back-off? Lock hierarchy is preferred in most cases. It is simpler, has no retry overhead, and is straightforward to reason about. Try-and-back-off is more flexible but less efficient (potential livelock if retries never succeed). Use hierarchy when a natural or imposed ordering exists.
Q7. What is the difference between deadlock and livelock? In a deadlock, threads are permanently blocked doing nothing. In a livelock, threads are not blocked but keep retrying the same failing operation — they are “alive” but making no progress. The try-and-back-off strategy can lead to livelock if both threads always release and retry simultaneously. Random delays help break this.

Leave a Reply

Your email address will not be published. Required fields are marked *