EmbeddedRelated.com

Deadlock

Category: Rtos | Also known as: deadlocks

A deadlock is a situation in which two or more tasks are each waiting for a resource held by another, such that none can proceed and all remain blocked indefinitely. It is a permanent stall absent external intervention, not a temporary delay.

In practice

Deadlocks appear most often in RTOS-based firmware that uses mutexes or other blocking synchronization primitives, though the problem applies broadly to any concurrent software. The classic pattern involves two tasks and two mutexes: Task A acquires Mutex 1 then tries to acquire Mutex 2, while Task B has already acquired Mutex 2 and is waiting on Mutex 1. Neither task can release what the other needs, so both hang forever. This pattern is discussed in detail in "Mutex vs. Semaphores – Part 2: The Mutex & Mutual Exclusion Problems."

Four conditions must hold simultaneously for a deadlock to occur: mutual exclusion (a resource can only be held by one task at a time), hold-and-wait (a task holds at least one resource while waiting for another), no preemption (resources are released voluntarily, not forcibly), and circular wait (a cycle of tasks each waiting on the next). Breaking any one of these conditions is the classical strategy for preventing deadlock, though the effectiveness of each approach depends on the resource model and implementation. A common mitigation is lock ordering: always acquire multiple mutexes in a globally agreed order so a circular wait cannot form.

In embedded systems, deadlocks can be especially hard to detect because the system does not crash outright. Instead, affected tasks silently stop making progress while unrelated tasks and interrupts may continue running normally. If a watchdog timer is not monitoring the stalled tasks, the deadlock can go unnoticed until a safety-critical function fails to respond. Designing watchdogs to detect task-level liveness, not just overall CPU activity, is a common countermeasure.

Deadlock risk is one of the costs introduced when adding synchronization primitives to a design. The post "Blocking == Technical Debt" explores the broader idea that every blocking call adds a category of hard-to-test failure modes. In resource-constrained or safety-critical designs, some teams deliberately avoid shared mutexes between multiple tasks, instead using message-passing or single-owner resource patterns that eliminate the hold-and-wait condition entirely.

Frequently asked

How is a deadlock different from a livelock or starvation?
In a deadlock, all involved tasks are completely blocked and no progress occurs anywhere in that group. In a livelock, tasks are actively running and changing state but making no useful progress (for example, each continuously yields to the other). In starvation, a task is perpetually denied a resource because higher-priority tasks always acquire it first, but the blocking tasks themselves are not waiting on each other.
Does priority inheritance prevent deadlock?
No. Priority inheritance prevents priority inversion (a high-priority task being indirectly delayed by a low-priority one), but it does not break the circular-wait condition that causes deadlock. Two tasks can still deadlock even with priority inheritance enabled on every mutex involved.
How can I detect a deadlock at runtime on a bare-metal RTOS target?
A common approach is to use timed mutex acquisition calls (for example, xSemaphoreTake with a finite timeout in FreeRTOS) instead of blocking indefinitely. If a timeout fires unexpectedly, the task can log a fault and trigger a watchdog reset or assert. Dedicated RTOS-aware debuggers (such as Tracealyzer or SEGGER SystemView) can also visualize task states and blocked resource chains, making circular waits visible.
What is lock ordering, and does it always prevent deadlock?
Lock ordering is a convention where all tasks that need multiple mutexes always acquire them in the same predefined sequence (for example, always Mutex A before Mutex B). This eliminates the circular-wait condition and prevents deadlock for that set of mutexes, provided every code path that touches those resources follows the ordering. It works reliably, but requires discipline across the entire codebase and can become difficult to enforce as a project grows.
Can deadlock occur with only one task and one mutex?
Yes, if the mutex is non-recursive and the same task attempts to acquire it twice without releasing it in between. On many RTOS implementations, a task that tries to take a non-recursive mutex it already holds will block on itself indefinitely, though exact behavior varies by RTOS and mutex type: some implementations detect self-deadlock and return an error instead. Recursive mutexes exist specifically to allow re-entrant acquisition by the same task, at the cost of a matching number of release calls.

Differentiators vs similar concepts

Deadlock is frequently confused with priority inversion. Priority inversion is a scheduling anomaly where a high-priority task is delayed because a low-priority task holds a needed resource; the high-priority task is blocked but the system is not permanently stuck. Deadlock means no involved task can proceed without external intervention such as a timeout, deadlock detection mechanism, or reset. Priority inheritance and priority ceiling protocols address priority inversion but do not prevent deadlock. Deadlock is also distinct from starvation, where a task can in principle proceed but is repeatedly denied CPU time or a resource by competing tasks.