If you've ever written firmware where two tasks share the same peripheral, you've had to ask how to protect it from concurrent access. Get the answer wrong, and the bug that shows up won't be obvious. Two tasks share an SPI bus; one uses a semaphore instead of a mutex to guard it, and under load the reads start coming back garbled, randomly, only under certain timing—the kind of bug that eats a sprint before anyone traces it back to a single wrong synchronization primitive.
A mutex is an ownership-based lock that allows a single task to use a shared resource. A semaphore is a counting signal that a task can use for event coordination, or that multiple tasks can use to share access to a pool of identical resources.
This article describes the differences between a semaphore and a mutex. It also explains when to use each in RTOS-based code and includes several common semaphore-use mistakes that can cause race conditions.
What is the difference between a mutex and a semaphore?
A mutex is an ownership-based lock that only the task that acquired it can release, while a semaphore is a signaling counter that any task, or even an interrupt service routine, can increment or decrement.
Many RTOS vendors and textbooks describe a mutex as "a binary semaphore," and that framing is where most of the confusion starts. Ownership makes a mutex a distinct primitive, not a special case of a semaphore. A counting semaphore initialized to 1 might look like a mutex on paper, but it has no owner and no built-in priority inheritance: two properties that turn out to matter a great deal in real-time firmware.
A mutex has two other properties that matter in real-time systems: ISR safety and priority inversion. Both come directly from the same root cause — ownership. Because a mutex has an owner and a semaphore doesn't, everything else in this article follows from that one distinction.
| Attribute | Mutex | Semaphore |
|---|---|---|
| Ownership | Owned by the task that locked it | No owner |
| Purpose | Mutual exclusion for one shared resource | Signaling and resource counting |
| Can be given from an ISR | Generally no | Yes (binary/counting, via a FromISR API) |
| Priority inheritance | Yes, in most RTOS implementations | No |
| Misuse detection | Often asserts on unlock by a non-owner | Fails silently if misused |
Getting synchronization primitives right is easier with the right partner. Lemberg Solutions' embedded software development team helps teams design RTOS firmware that holds up under real production load.

How does ownership work in a mutex vs a semaphore?
A task that acquires a mutex can only release that mutex, whereas a semaphore has no owner, so any task or even an ISR can release a semaphore.
In practice, you can give a binary semaphore to another task, but not a mutex. A binary semaphore can interrupt a service routine (ISR) through a special FromISR API. This is the correct tool to signal a task that data are ready in an interrupt.
Ownership also makes misuse easier to catch during development. Many RTOS kernels can assert if a task tries to unlock a mutex it doesn't hold, surfacing the bug immediately. A wrongly given semaphore has no owner to check against; it succeeds, and the resulting corruption surfaces elsewhere, later and harder to trace.
What is priority inversion, and why does it matter for mutex vs semaphore?
Priority inversion happens when a low-priority task holds a lock while a high-priority task waits for it. The high-priority task stalls behind a task that, by design, should never be able to hold it up.
NASA's Mars Pathfinder rover ran into exactly this issue in 1997. Its intermittent resets are widely attributed to unbounded priority inversion, and the case is cited often enough in embedded engineering circles to count as the canonical example of what happens when the problem goes unmanaged.
Priority inheritance is the mechanism that stops it. The RTOS temporarily raises the priority of the task holding the lock, so that task finishes its critical section and releases the lock before the high-priority task waits any longer. A mutex is the only primitive built to support this, because it's the only one with an owner, a specific task the scheduler can identify and boost.
A semaphore has no such owner. With nothing to boost, priority inheritance simply isn't available, and priority inversion has no built-in fix. That gap is a real design risk to plan around, not a textbook footnote.
| Primitive | Priority inversion risk | Mitigation available |
|---|---|---|
| Mutex | Present, but bounded | Priority inheritance (built into most RTOS mutex implementations) |
| Semaphore | Present, unbounded | None; no owner to boost |
When should you use a mutex vs a semaphore in embedded systems?
Use a mutex to protect a single shared resource from concurrent access by tasks; and use a semaphore to signal events between tasks and ISRs or to manage a countable pool of identical resources.
When a mutex is suitable: a shared SPI or I2C bus driver; a shared UART transmit buffer, or a linked list that more than one task writes to. In each case, exactly one task should hold the resource at a time, and that same task should take and release the lock.
Binary semaphore is suitable: an ISR signaling a task that new sensor data has arrived, or a task-to-task handoff where the signaler and the waiter are different tasks by design. A mutex can't express that pattern, since a mutex must be released by whoever locked it.
A counting semaphore can manage a fixed-size connection pool (e.g., for connecting to a server) and control how many tasks can use the same resources.
For a concrete anchor, FreeRTOS exposes the split directly in its API: xSemaphoreCreateMutex() for the ownership-based lock versus xSemaphoreCreateBinary() or xSemaphoreCreateCounting() for the signaling primitives, three distinct constructors for what looks, at a glance, like one concept.

Getting the choice between mutex and semaphore right at the driver level often decides whether a board support package is stable under load or intermittently flaky. Lemberg Solutions' firmware development team works through exactly this kind of synchronization design on embedded and RTOS-based projects.
| Scenario | Use mutex? | Use semaphore? | Why |
|---|---|---|---|
| Shared SPI/I2C bus | Yes | No | Single resource, same task takes and releases it |
| ISR signals new sensor data | No | Yes (binary) | ISR can't own a mutex; signaling needs no owner |
| Task-to-task handoff (different signaler/waiter) | No | Yes (binary) | Ownership model doesn't fit a cross-task signal |
What are common mistakes when using mutexes and semaphores in embedded systems?
Most production bugs come because of releasing a mutex from the wrong context, holding a mutex across a blocking call, or treating a counting semaphore as a substitute for a mutex and losing priority inheritance in the process.
- Releasing a mutex from the wrong context. Giving a mutex from an ISR, or from a task that never took the lock, is the mistake to watch for first. Most RTOS kernels either reject the call outright or trigger an assertion that halts execution before things get worse. Without asserts enabled, though, nothing warns you that the lock state gets corrupted, and the bug shows up later, somewhere completely unrelated.
- Deadlock from inconsistent lock order. Two tasks lock the same two mutexes in opposite order: task A locks mutex 1, then waits on mutex 2; task B locks mutex 2, then waits on mutex 1. Neither task can proceed. To fix this issue, engineers need a proper, documented lock-acquisition order.
- Holding a mutex longer than necessary. Holding a mutex across a blocking operation — a delay, a wait on another semaphore, a slow peripheral transaction — extends the window for priority inversion, even with priority inheritance enabled.
Treating a semaphore as a mutex substitute comes up constantly in engineer discussions on forums like r/embedded, where it's one of the most-repeated mix-ups in RTOS Q&A threads. This is a sign of that the issue trips up developers moving from general-purpose multithreading into RTOS work, rather than being an obscure edge case. See our comparisons of FreeRTOS vs. Zephyr project and choosing the right RTOS for more on the real-time operating system these primitives run on.

Conclusion
The mutex-vs-semaphore mix-up survives because the two primitives share an API shape, such as take, give, block, while solving different problems underneath. A mutex is about ownership and mutual exclusion; a semaphore is about signaling and counting.
Use a mutex to lock a resource that only one task needs for a period. Use a semaphore to signal events: from an ISR or a task, or to manage a pool of a countable number of resources. Making that call correctly at design time is far less costly than fixing the intermittent corruption or deadlocks that incorrect use of these primitives brings later in the project.