freertos-mutex-exclusion-mutua

Mutex in FreeRTOS: protecting shared resources

  • 6 min

A mutex is a mutual exclusion mechanism that allows protecting a shared resource so that only one task can use it at a time.

In the previous article we saw how to use semaphores to have one task wait for another. Today we are going to address a different, more dangerous and subtle problem: when two tasks fight over the same thing.

Think of two tasks building a message through several calls to Serial.print. Although the driver protects individual calls, the complete sequence can be interleaved with that of another task:

  • Task A wants to send: “HELLO WORLD”
  • Task B wants to send: “ERROR 404”
  • Console result: “HE-ER-LLO -WO-RL-40-D4”

This is a Race Condition. And it doesn’t only happen with the serial port; it happens with global variables, I2C buses, SPI displays, etc.

To avoid this chaos, we need a Mutual Exclusion mechanism. In FreeRTOS, this mechanism is called a Mutex.

What is a mutex?

A Mutex is a “token” or unique key that protects a shared resource. It works under a simple premise:

“Whoever holds the Mutex has the right to use the resource. Whoever doesn’t, must wait in the queue until the Mutex is released.”

It is very similar to the key to a gas station restroom:

  1. You arrive and see the key hanging (Mutex Free).
  2. You grab it and enter the restroom (Mutex Taken).
  3. If someone else arrives while you’re inside, they see there’s no key and wait outside (Task Blocked).

Differences between mutex and binary semaphore

Technically, a Mutex looks like a Binary Semaphore that starts full. However, there are two fundamental differences that mean we should NOT interchange them:

  1. Concept of Ownership: A Binary Semaphore can be taken by one task and released by another (or by an interrupt). A Mutex must be released by the same task that took it.
  2. Priority Inheritance: Mutexes implement a mechanism to limit priority inversion (explained below). Binary semaphores do not.
CharacteristicBinary SemaphoreMutex
Main UseSynchronization (Events)Protection (Mutual Exclusion)
Initial StateEmpty (Taken)Full (Given/Free)
OwnerNo ownerThe task that takes it is the owner
From ISRYES (FromISR)NO (Forbidden)
Priority InheritanceNoYes

Important idea:

  • Do you want to synchronize (notify about events)? Use Binary Semaphore.
  • Do you want to protect a variable/resource? Use Mutex.
  • Never use a Mutex inside an Interrupt (ISR).

Mutex API in FreeRTOS

It is very similar to the semaphore API, but with specific functions.

SemaphoreHandle_t xMutex;

void setup() {
    // 1. Create the Mutex
    xMutex = xSemaphoreCreateMutex();
    
    // Unlike the binary one, the Mutex is born "FREE" (Given).
    // Ready to be used.
}
Copied!

To protect a Critical Section (the piece of code that touches the shared resource):

void TaskPrint(void *pvParameters) {
    for(;;) {
        // 1. Try to grab the key (Wait forever)
        if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            
            // --- START CRITICAL SECTION ---
            // Only one task can be here at a time.
            Serial.print("Long message ");
            Serial.print("that must not ");
            Serial.println("be interrupted.");
            // --- END CRITICAL SECTION ---
            
            // 2. IMPORTANT: Return the key
            xSemaphoreGive(xMutex);
        }
        
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}
Copied!

The Priority Inversion Problem

Here the Mutex makes a significant difference. Let’s consider this uncomfortable scenario with three tasks: High (H), Medium (M) and Low (L).

  1. L (Low) runs and takes the Mutex to write to the EEPROM.
  2. Suddenly, the Scheduler wakes up H (High).
  3. H tries to take the EEPROM Mutex, but L holds it. So H blocks, waiting.
  4. Now comes the problem: M (Medium) wakes up. Since M has higher priority than L, the Scheduler preempts the CPU from L and starts running M.

Result:

  • H is waiting for L.
  • L cannot finish and release the Mutex because M doesn’t let it run.
  • In practice: The medium priority task is blocking the high priority task!

This is called Priority Inversion and has caused famous failures (like the NASA Mars Pathfinder mission).

The solution: priority inheritance

FreeRTOS mutexes reduce this blocking through priority inheritance:

When a High Priority task starts waiting for a Mutex held by a Low Priority task, the Scheduler temporarily raises the priority of the Low task to match the High one.

In the previous example:

  1. When H tries to take the Mutex that L holds, FreeRTOS boosts L’s priority to H’s level.
  2. Now L is more important than M, so M cannot interrupt it.
  3. L finishes quickly, releases the Mutex, and regains its original low priority.
  4. H takes the Mutex and continues.

Binary semaphores do not do this. Inheritance does not eliminate the time the high-priority task waits for the resource, but it prevents intermediate tasks from unnecessarily prolonging that block.

Recursive Mutexes

Sometimes, a function takes a Mutex and then calls another function… which tries to take the same Mutex.

void AuxiliaryFunction() {
    xSemaphoreTake(myMutex, portMAX_DELAY); // <--- Eternal block! (Deadlock)
    // ...
    xSemaphoreGive(myMutex);
}

void MainFunction() {
    xSemaphoreTake(myMutex, portMAX_DELAY);
    AuxiliaryFunction(); // Calling while already holding the key
    xSemaphoreGive(myMutex);
}
Copied!

If we use a normal mutex, the task will block when trying to take it a second time. The kernel knows the owner, but a normal mutex does not support recursive takes.

This is why Recursive Mutexes (xSemaphoreCreateRecursiveMutex) exist.

  • They allow the same task to take the Mutex multiple times.
  • To release it, it must call Give as many times as it called Take.

The danger of deadlock

Although Mutexes prevent race conditions, they introduce a new risk: Deadlock.

It occurs when two tasks mutually block each other waiting for resources that the other holds.

  1. Task A holds Mutex 1 and wants Mutex 2.
  2. Task B holds Mutex 2 and wants Mutex 1.
  3. Nobody ever makes progress.

Solution: Try to take mutexes in the same order in all tasks. A timeout also allows abandoning the attempt, provided you release what you had already acquired before retrying.