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:
- Thread A locks
mutex_1and attempts to acquiremutex_2. - Concurrently, Thread B locks
mutex_2and attempts to acquiremutex_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_aIf 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 itselfWhen 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.