Deadlocks with Multiple Mutexes in C Explained

Concurrent applications written in C frequently use POSIX threads (pthreads) and mutexes to protect shared data across threads. A deadlock occurs when two or more threads are permanently blocked, each waiting for a mutex held by the other, bringing execution to a standstill. Understanding why deadlocks occur when managing multiple mutexes requires examining how lock ordering, thread scheduling, and resource allocation interact at the system level.

Inconsistent Lock Ordering (Circular Wait)

The most frequent cause of deadlocks in multi-threaded C applications is acquiring multiple mutexes in an inconsistent sequence. For a deadlock to happen via circular wait:

  1. Thread A locks mutex_1 and attempts to acquire mutex_2.
  2. Concurrently, Thread B locks mutex_2 and attempts to acquire mutex_1.

Because pthread_mutex_lock() is a blocking call by default, Thread A cannot proceed until mutex_2 is released, and Thread B cannot proceed until mutex_1 is released. Neither thread can release its acquired mutex, resulting in an indefinite stall.

Hold and Wait Behavior

A deadlock requires that a thread hold at least one resource while waiting to acquire another. In C, standard mutexes permit this behavior natively:

pthread_mutex_lock(&mutex_a);
// Resource A is held
pthread_mutex_lock(&mutex_b); // Thread blocks here while still holding mutex_a

If another thread holds mutex_b and attempts to lock mutex_a, both threads fulfill the "hold and wait" condition. Without a mechanism to release mutex_a when mutex_b is unavailable—such as falling back to pthread_mutex_trylock()—the thread retains its lock indefinitely.

Lack of Lock Preemption

Standard POSIX mutexes (PTHREAD_MUTEX_NORMAL or PTHREAD_MUTEX_DEFAULT) cannot be preempted. Once a thread successfully invokes pthread_mutex_lock(), the operating system will not forcibly revoke that lock, nor can another thread invalidate it. Because locks cannot be preempted, the cycle cannot be broken externally without terminating the process or thread.

Non-Recursive Self-Deadlocks

A deadlock can also occur within a single thread when attempting to reacquire a mutex it already owns. If a mutex is initialized as non-recursive (the standard behavior in C):

pthread_mutex_lock(&mutex_a);
pthread_mutex_lock(&mutex_a); // Deadlock: thread waits for itself

When managing multiple mutexes across modular functions, a thread might lock mutex_a, call a nested helper function, and that helper function may inadvertently attempt to lock mutex_a again, causing the thread to block on itself.

Early Exits and Asymmetric Unlocking

C lacks built-in language constructs like RAII (Resource Acquisition Is Initialization) found in C++. Developers must manually release locks using pthread_mutex_unlock(). When functions manage multiple mutexes alongside multiple exit points—such as return, goto, or break statements on error conditions—omitting an unlock call on a single path leaves a mutex permanently locked. When another thread later attempts to acquire that abandoned mutex while holding a different one, the application deadlocks.