Un mutex es un mecanismo de exclusión mutua que permite proteger un recurso compartido para que solo una tarea lo use cada vez.
En el artículo anterior vimos cómo usar semáforos para que una tarea espere a otra. Hoy vamos a tratar un problema diferente, más peligroso y sutil: cuando dos tareas se pelean por lo mismo.
Piensa en dos tareas construyendo un mensaje mediante varias llamadas a Serial.print. Aunque el driver proteja llamadas individuales, la secuencia completa puede intercalarse con la de otra tarea:
- Tarea A quiere enviar: “HOLA MUNDO”
- Tarea B quiere enviar: “ERROR 404”
- Resultado en consola: “HO-ER-LA-ROR 4-MUN-04-DO”
Esto es una Condición de Carrera (Race Condition). Y no solo pasa con el puerto serie; pasa con variables globales, buses I2C, pantallas SPI, etc.
Para evitar este caos, necesitamos un mecanismo de Exclusión Mutua (MUTual EXclusion). En FreeRTOS, este mecanismo se llama Mutex.
¿Qué es un mutex?
Un Mutex es un “token” o llave única que protege un recurso compartido. Funciona bajo una premisa simple:
“El que tiene el Mutex, tiene derecho a usar el recurso. El que no, debe esperar en la cola hasta que el Mutex sea liberado.”
Es muy similar a la llave del baño de una gasolinera:
- Llegas y ves la llave colgada (Mutex Libre).
- La coges y entras al baño (Mutex Tomado).
- Si llega otro mientras estás dentro, ve que no hay llave y se espera fuera (Tarea Bloqueada).
Diferencias entre mutex y semáforo binario
Técnicamente, un Mutex parece un Semáforo Binario que empieza lleno. Sin embargo, hay dos diferencias fundamentales que hacen que NO debamos intercambiarlos:
- Concepto de Propiedad: Un Semáforo Binario puede ser tomado por una tarea y liberado por otra (o por una interrupción). Un Mutex debe ser liberado por la misma tarea que lo tomó.
- Herencia de prioridad: Los mutex implementan un mecanismo para limitar la inversión de prioridad (lo explicamos más abajo). Los semáforos binarios no.
| Característica | Semáforo Binario | Mutex |
|---|---|---|
| Uso Principal | Sincronización (Eventos) | Protección (Exclusión Mutua) |
| Estado Inicial | Vacío (Taken) | Lleno (Given/Libre) |
| Dueño | No tiene dueño | La tarea que hace Take es dueña |
| Desde ISR | SÍ (FromISR) | NO (Prohibido) |
| Herencia Prioridad | No | Sí |
Idea importante:
- ¿Quieres sincronizar (avisar de eventos)? Usa Semáforo Binario.
- ¿Quieres proteger una variable/recurso? Usa Mutex.
- Nunca uses un Mutex dentro de una Interrupción (ISR).
API de mutex en FreeRTOS
Es muy similar a la de los semáforos, pero con funciones específicas.
SemaphoreHandle_t xMutex;
void setup() {
// 1. Crear el Mutex
xMutex = xSemaphoreCreateMutex();
// A diferencia del binario, el Mutex nace "LIBRE" (Given).
// Listo para usarse.
}Para proteger una Sección Crítica (el trozo de código que toca el recurso compartido):
void TareaImprimir(void *pvParameters) {
for(;;) {
// 1. Intentamos coger la llave (Wait forever)
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// --- INICIO SECCIÓN CRÍTICA ---
// Solo una tarea puede estar aquí a la vez.
Serial.print("Mensaje largo ");
Serial.print("que no debe ");
Serial.println("cortarse.");
// --- FIN SECCIÓN CRÍTICA ---
// 2. IMPORTANTE: Devolver la llave
xSemaphoreGive(xMutex);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}El problema de la inversión de prioridad
Aquí el Mutex marca una diferencia importante. Pensemos en este escenario incómodo con tres tareas: Alta (H), Media (M) y Baja (L).
- L (Baja) se ejecuta y coge el Mutex para escribir en la EEPROM.
- De repente, el Scheduler despierta a H (Alta).
- H intenta coger el Mutex de la EEPROM, pero lo tiene L. Así que H se bloquea esperando.
- Ahora viene el problema: Despierta M (Media). Como M tiene más prioridad que L, el Scheduler apropia la CPU a L y pone a ejecutar a M.
Resultado:
- H está esperando a L.
- L no puede terminar y soltar el Mutex porque M no le deja ejecutar.
- En la práctica: ¡La tarea de prioridad Media está bloqueando a la tarea de prioridad Alta!
Esto se llama Inversión de Prioridad y ha causado fallos famosos (como en la misión Mars Pathfinder de la NASA).
La solución: herencia de prioridad
Los mutex de FreeRTOS reducen este bloqueo mediante herencia de prioridad:
Cuando una tarea de Alta Prioridad empieza a esperar por un Mutex que tiene una tarea de Baja Prioridad, el Scheduler eleva temporalmente la prioridad de la tarea Baja hasta igualarla con la Alta.
En el ejemplo anterior:
- Cuando H intenta coger el Mutex que tiene L, FreeRTOS sube la prioridad de L al nivel de H.
- Ahora L es más importante que M, así que M no puede interrumpirla.
- L termina rápido, suelta el Mutex y recupera su prioridad baja original.
- H coge el Mutex y continúa.
Los semáforos binarios no hacen esto. La herencia no elimina el tiempo durante el que la tarea alta espera al recurso, pero evita que tareas intermedias prolonguen innecesariamente ese bloqueo.
Mutex recursivos
A veces, una función toma un Mutex y luego llama a otra función… que intenta tomar el mismo Mutex.
void FuncionAuxiliar() {
xSemaphoreTake(miMutex, portMAX_DELAY); // <--- ¡Bloqueo eterno! (Deadlock)
// ...
xSemaphoreGive(miMutex);
}
void FuncionPrincipal() {
xSemaphoreTake(miMutex, portMAX_DELAY);
FuncionAuxiliar(); // Llamamos teniendo ya la llave
xSemaphoreGive(miMutex);
}Si usamos un mutex normal, la tarea se bloqueará al intentar tomarlo por segunda vez. El kernel conoce al propietario, pero un mutex normal no admite tomas recursivas.
Para esto existen los Mutex Recursivos (xSemaphoreCreateRecursiveMutex).
- Permiten que la misma tarea tome el Mutex múltiples veces.
- Para liberarlo, debe hacer
Givetantas veces como hizoTake.
El peligro del deadlock
Aunque los Mutex evitan condiciones de carrera, introducen un nuevo riesgo: el Deadlock.
Ocurre cuando dos tareas se bloquean mutuamente esperando recursos que tiene la otra.
- Tarea A tiene Mutex 1 y quiere Mutex 2.
- Tarea B tiene Mutex 2 y quiere Mutex 1.
- Nadie avanza nunca.
Solución: Intenta tomar los mutex en el mismo orden en todas las tareas. Un timeout también permite abandonar el intento, siempre que liberes lo que ya habías adquirido antes de reintentarlo.