Embedded

Semaphore vs mutex in RTOS

By CourseTron Team ·  7 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: A mutex is an ownership-based lock for mutual exclusion (one task holds it and the same task releases it, with priority inheritance to tame priority inversion); a semaphore is an ownerless counter used for signaling and resource counting — so you lock shared data with a mutex and you synchronize or hand off events with a semaphore.

    Ask any embedded interviewer to separate strong candidates from weak ones and "What is the difference between a semaphore and a mutex?" is almost guaranteed to come up. Both are kernel objects in every RTOS — FreeRTOS, Zephyr, ThreadX, µC/OS-III — and both can block a task. But they solve different problems, and confusing them is a classic source of hard-to-reproduce field bugs. Let's build the distinction from first principles with concrete numbers.

    Two problems that look alike

    Concurrency bugs come in two flavours. The first is the race condition: two tasks touch the same variable, buffer, or peripheral register at overlapping times and corrupt it. The second is coordination: task B must not run until task A has produced something — an ADC sample finished, a byte arrived on the UART, a frame was assembled. Mutual exclusion solves the first; signaling solves the second. A mutex is the right tool for the former, a semaphore for the latter.

    Rule of thumb: if you are protecting data, reach for a mutex. If you are announcing that an event happened, reach for a semaphore. The keyword "protect" vs "signal" decides it.

    Ownership: the defining difference

    The single most important technical distinction is ownership. A mutex is recursive-capable and owned: the task that takes it becomes its owner, and only that same task may give it back. This is what makes the mutex a true lock. A binary semaphore, by contrast, is just a flag with a count of 0 or 1 — any task (or an ISR) can "give" it, regardless of who last "took" it. That asymmetry is not a detail; it is the whole point.

    Because the kernel knows a mutex's owner, it can do something a semaphore cannot: priority inheritance. Consider the FreeRTOS calls. xSemaphoreTake() and xSemaphoreGive() operate on both, but a mutex created with xSemaphoreCreateMutex() records the holder's task handle, while xSemaphoreCreateBinary() records nothing. You must never give a mutex from an ISR — an interrupt has no task identity to own it — whereas giving a binary semaphore from an ISR (via xSemaphoreGiveFromISR()) is the canonical "defer work to a task" pattern.

    Mutex: owned lock Semaphore: signal Task A (owner) Task B (waits) Shared data blocked take, use, then the same task gives back ISR / giver Worker task give (count++) any task or ISR may give CourseTron · coursetron.com
    Mutex: the same task that takes the lock releases it. Semaphore: giver and taker are different actors, and the giver can even be an interrupt.

    Priority inversion — and why interviewers love it

    Priority inversion is the scenario that makes mutexes special. Suppose a low-priority task L holds a lock on a shared SPI bus. A high-priority task H then becomes ready and tries to take the same lock, so H blocks. So far, acceptable. But now a medium-priority task M (which needs no lock) preempts L. L cannot run, so it never releases the lock, so H — the highest-priority task in the system — is effectively blocked by M. The priorities are inverted.

    This is not academic. The Mars Pathfinder rover suffered repeated watchdog resets in 1997 from exactly this pattern on its VxWorks-based system: a high-priority bus-management task was starved by a medium task while a low task held a mutex. The fix was to enable priority inheritance on that mutex.

    With priority inheritance, while H waits on a mutex held by L, the kernel temporarily boosts L to H's priority so L finishes and releases quickly. A plain binary semaphore has no owner to boost — which is precisely why you should not use a semaphore for mutual exclusion.

    Priority inversion timeline H (high) M (medium) L (low) H waits on M: inversion holds lock H blocked on lock M runs (no lock) L gives H runs time Inheritance boosts L at the dashed line, so M cannot preempt it CourseTron · coursetron.com
    Timeline: L holds the lock, M preempts L, and H stays blocked until L releases the lock.

    Counting semaphores: managing a pool

    A counting semaphore generalizes the binary case to an initial count N, modelling N identical resources. Classic example: a pool of 3 DMA channels. Initialize the semaphore to 3; each task that grabs a channel takes it (count decrements), each release gives it (count increments). A fourth task that asks while all three are busy blocks until one frees up. The same pattern governs a bounded producer–consumer ring buffer: one counting semaphore counts filled slots, another counts empty slots.

    Counting semaphore: pool of 3 tokens Resource pool count = 3 2 Worker task take(): count-- Count hits 0: the next take blocks until a give returns a token CourseTron · coursetron.com
    A counting semaphore hands out tokens from a finite pool; a task blocks when the count hits zero and resumes when a token is returned.

    Side-by-side

    PropertyMutexSemaphore (binary/counting)
    Primary useMutual exclusion (protect data)Signaling / resource counting
    OwnershipYes — taker must releaseNo — any task/ISR gives
    Give from ISRNoYes
    Priority inheritanceYes (if enabled)No
    Recursive takeSupported (recursive mutex)No
    Typical countAlways 0/10/1 or 0…N

    If you want to see how these objects slot into real task bodies, timing budgets, and blocking states, our deep dive on FreeRTOS tasks and scheduling walks through the scheduler decisions, and priority inversion and deadlock covers the failure modes in detail. For hands-on labs, see the full CourseTron course catalogue. Teams building production firmware often pair training with design review from Avecas VLSI and embedded design services.

    Common pitfalls

    • Using a binary semaphore as a lock. It works until the day priority inversion bites — then you get a missed deadline that reproduces once a week.
    • Giving a mutex from an ISR. Undefined behaviour; the ISR is not the owner.
    • Deadlock from lock ordering. Two tasks each hold one mutex and wait for the other. Always acquire multiple mutexes in a globally fixed order.
    • Forgetting the timeout. Prefer a bounded xSemaphoreTake(h, pdMS_TO_TICKS(50)) over an infinite wait so a stuck peer cannot hang the whole task forever.

    Ready to master RTOS synchronization with real FreeRTOS and Zephyr labs, oscilloscope-timed priority-inversion experiments, and interview drills? Enrol in CourseTron's Embedded Systems & RTOS track and build the instincts that pass the interview and ship the product.

    FAQ

    Can a mutex replace a semaphore?

    No. A mutex cannot be given by an ISR or a task other than its owner, so it cannot signal an event from an interrupt. Use a binary semaphore (or a task notification) for ISR-to-task signaling.

    What is a recursive mutex?

    A recursive mutex lets the owning task take it multiple times (incrementing an internal count) and requires an equal number of gives to release it. It avoids self-deadlock when a locked function calls another function that locks the same resource.

    Does priority inheritance eliminate priority inversion?

    It bounds it rather than eliminating it. Inheritance limits the inversion to the time the low-priority task needs to finish its critical section, instead of an unbounded delay. Priority ceiling protocols give a tighter bound still.

    Is a FreeRTOS task notification better than a binary semaphore?

    For single-task signaling it is often faster and uses less RAM because no separate kernel object is allocated. For many-to-one signaling or resource counting, a semaphore is still the right abstraction.

    Advertisement