Skip to main content

Command Palette

Search for a command to run...

RTOS Mutex

Updated
4 min readView as Markdown

0) One-line definition

Mutex = a lock to protect mutually exclusive critical sections.
The key feature is priority inversion prevention (inheritance)—this is what differentiates it from queues/semaphores.
Never use it in ISRs. (Task context only)


1) When to use—and when not to

  • ✅ Protect shared resources (driver internal state, global data structures, file/flash handles)

  • ✅ When tasks of different priorities contend for a resource (priority inheritance mitigates inversion)

  • Data passing / event signaling → use Queue / TaskNotify / MessageBuffer

  • Inside an ISR → there’s no FromISR API (have the ISR wake a task via semaphore/notify instead)

  • ❌ Holding it across long operations / blocking I/O → seeds latency and starvation


2) Minimal examples (FreeRTOS)

Dynamic creation

#include "FreeRTOS.h"
#include "semphr.h"

SemaphoreHandle_t m;

void init_mutex(void){
    m = xSemaphoreCreateMutex();         // requires configUSE_MUTEXES=1
    configASSERT(m != NULL);
}

void use_shared(void){
    if (xSemaphoreTake(m, pdMS_TO_TICKS(5)) == pdPASS) {
        // Critical section: keep it short; avoid printf/blocking calls
        critical_work();
        xSemaphoreGive(m);               // always pair with a Give
    } else {
        // Timeout: retry/backoff/error handling
        on_lock_timeout();
    }
}

Static creation

static StaticSemaphore_t mCtrl;
static SemaphoreHandle_t m;

void init_mutex_static(void){
    m = xSemaphoreCreateMutexStatic(&mCtrl);
    configASSERT(m != NULL);
}

Important

  • Only the owning task can xSemaphoreGive(mutex) (FreeRTOS tracks ownership).

  • There is no xSemaphoreGiveFromISR() for mutexes (mutexes are not allowed in ISRs).

  • Prefer short timeouts and design a failure path; don’t default to infinite waits.


3) Priority inversion & inheritance (core concept)

    • Scenario: L (low-priority) holds the mutex; H (high-priority) waits on the same mutex.

      • Inheritance: the kernel temporarily raises L’s priority up to H’s, so L finishes the critical section and releases the mutex sooner.

      • In FreeRTOS, this is automatic for mutex types (with configUSE_MUTEXES=1).

      • Binary semaphores don’t have priority inheritance → for exclusive access, use a mutex.


4) Rules to avoid deadlocks and excessive latency

  1. Global lock ordering

    • Enforce a single order A → B → C.

    • No reverse/cross acquisition. (prevents deadlocks)

  2. Minimize hold time

    • Inside the critical section, don’t wait/sleep/receive from queues/do blocking I/O.

    • Do heavy computation outside the lock.

  3. Timeout + retry policy

    • On xSemaphoreTake(m, timeout) failure, back off / retry, log/alert.

    • Blind portMAX_DELAY can mask deadlocks.

  4. Try-lock pattern

    • Use xSemaphoreTake(m, 0) to fail fast and do other work first.
  5. One lock at a time

    • Ideally hold only one mutex per critical section (if you must hold multiple, obey ordering).

5) Recursive mutex

  • When the same task must acquire the same mutex nested (recursion/library call chains):

      SemaphoreHandle_t rm = xSemaphoreCreateRecursiveMutex();
      xSemaphoreTakeRecursive(rm, timeout);
      // ...
      xSemaphoreGiveRecursive(rm);
    
  • Caution

    • You must Give as many times as you Take to fully release it.

    • Adds complexity—use only when truly necessary.


6) Mutex vs. (binary) semaphore — quick summary

ItemMutexBinary semaphore
PurposeExclusive resource protectionEvent signaling / simple access gating
Priority inheritanceYesNo
OwnershipOnly owner can GiveAnyone can Give
ISR useNot allowedGiveFromISR/TakeFromISR allowed (depending on use)

Bottom line: “Resource protection = Mutex”, “Signal/wake-up = Semaphore/Notify.”


8) Practical patterns & anti-patterns

✅ Good patterns

  • Lock-guard idioms to guarantee release:

      #define LOCK_OR_RETURN(mx, to) do{ if(xSemaphoreTake(mx, to)!=pdPASS) return; }while(0)
      #define UNLOCK(mx) do{ xSemaphoreGive(mx); }while(0)
    
      void foo(void){
          LOCK_OR_RETURN(m, pdMS_TO_TICKS(2));
          // ... critical ...
          UNLOCK(m);
      }
    
    • Touch only the data truly requiring the lock; do the rest outside.

      ❌ Bad patterns

      • Holding a mutex while doing vTaskDelay(), xQueueReceive(portMAX_DELAY), printf, flash writes, or any long operation.

      • Trying to use a mutex in an ISR (nope).

      • Using a binary semaphore for resource protection (no inheritance → inversion risk).

      • No lock ordering → deadlocks.


9) Priority/config checklist

  • FreeRTOS: configUSE_MUTEXES=1; for recursion, configUSE_RECURSIVE_MUTEXES=1.

  • Priority placement: tasks that frequently hold shared resources shouldn’t be too low in priority (even with inheritance, latency grows).

  • Very short critical sections can use taskENTER_CRITICAL(), but that delays ISRs—keep it very short.


10) Debugging / observability

  • Name your mutexes:
    vQueueAddToRegistry((QueueHandle_t)m, "MutexFoo"); to identify them in traces/debuggers.

  • Measure hold time: log ticks before/after acquisition; refactor if excessive.

  • Contention metrics: timeouts/fail counts, avg/max wait times.


Final checklist

  • Never use a mutex in an ISR

  • Mutex for resource protection, Semaphore/Notify for signaling

  • Document/enforce global lock ordering in code reviews

  • Design timeouts/backoff (avoid infinite waits)

  • Keep critical sections short; avoid blocking inside

  • Use recursive mutex only when necessary (avoid overuse)

More from this blog

psk-study

134 posts