Los problemas de concurrencia son fallos que aparecen cuando varias tareas interactúan con los mismos recursos o esperan unas de otras.
Hasta ahora hemos visto las herramientas que nos ofrece FreeRTOS para poner orden en el caos: Colas, Semáforos y Mutex. Son herramientas muy útiles, pero al introducir concurrencia también abrimos la puerta a bugs bastante difíciles de depurar.
El sistema no da un error de compilación, ni siquiera un crash inmediato. Simplemente se detiene, responde tarde o se vuelve lento a ratos. Hoy vamos a hablar de dos clásicos: la Inversión de Prioridad y el Deadlock (bloqueo mutuo).
Inversión de prioridad
Este fenómeno ocurre cuando una tarea de alta prioridad se ve obligada a esperar a una tarea de baja prioridad, rompiendo la lógica del sistema de prioridades.
Ya lo mencionamos brevemente en el artículo anterior sobre Mutex, pero vamos a diseccionarlo porque es importante entender la secuencia de eventos que lleva al desastre.
Cómo se produce
Imagina tres tareas con prioridades distintas:
- Tarea H (High): Alta prioridad (Crítica).
- Tarea M (Medium): Prioridad Media (Procesamiento).
- Tarea L (Low): Prioridad Baja (Mantenimiento).
Y un recurso compartido protegido por un semáforo (digamos, un bus I2C).
- L está ejecutándose (porque nadie más quiere CPU) y toma el semáforo del I2C.
- En ese momento, llega una interrupción y despierta a H.
- H se apropia de la CPU, interrumpe a L y empieza a ejecutarse.
- H intenta usar el I2C, pero ve que está ocupado. Como es un recurso protegido, H se bloquea esperando a que se libere.
- La CPU vuelve a L para que termine y suelte el semáforo.
Hasta aquí todo es normal. H tiene que esperar un poco a L. Es el precio de compartir recursos.
El Problema
Mientras L tiene el semáforo y H está esperando… ¡Despierta M!
- Como M tiene más prioridad que L, el Scheduler detiene a L y pone a M.
- M se ejecuta felizmente. No necesita el I2C, así que sigue a lo suyo.
- M puede tardar 500ms en terminar.
¿El resultado?
- H está esperando a L.
- L está esperando a que M le deje la CPU.
- En la práctica: H (Alta) está esperando a M (Media).
Hemos invertido las prioridades. Una tarea media está bloqueando indefinidamente a la tarea crítica, simplemente porque la tarea baja tiene un recurso secuestrado y no le dejan soltarlo.
Este problema casi hizo fracasar la misión Mars Pathfinder de la NASA en 1997. El sistema se reiniciaba constantemente porque una tarea de información meteorológica (Media) impedía que una tarea de gestión de bus (Baja) soltara un Mutex que necesitaba la tarea de control principal (Alta).
Herencia de prioridad
Como vimos, la solución es usar Mutex en lugar de Semáforos Binarios para proteger recursos.
FreeRTOS implementa Priority Inheritance en los Mutex:
- Cuando H se bloquea esperando a L, el kernel eleva temporalmente la prioridad de L hasta igualarla con H.
- Ahora, cuando M intenta ejecutarse, no puede, porque L (ahora disfrazada de Alta prioridad) es más importante.
- L termina rápido, suelta el Mutex y recupera su prioridad baja original.
- H toma el relevo.
Si proteges un recurso, usa xSemaphoreCreateMutex. Ten en cuenta que la herencia reduce la inversión, pero no elimina el tiempo de espera ni sustituye al análisis temporal. Los diseños con varios mutex anidados requieren especial cuidado.
Deadlock o bloqueo mutuo
El Deadlock es un escenario aún peor. En la inversión de prioridad el sistema va lento; en el Deadlock, el sistema se congela para siempre.
Ocurre cuando dos (o más) tareas se bloquean mutuamente esperando un recurso que tiene la otra. Es un círculo vicioso del que nadie puede salir.
Cómo se produce
Imagina dos recursos: WiFi y tarjeta SD, protegidos por dos mutex (mutWiFi, mutSD).
Y dos tareas:
- Tarea A: Quiere guardar un log en la SD y luego enviarlo por WiFi.
- Tarea B: Quiere descargar un archivo por WiFi y guardarlo en la SD.
// Pseudocódigo del Desastre
void TareaA() {
take(mutSD); // Tengo la SD
// ... Context Switch justo aquí ...
take(mutWiFi); // Intento pillar el WiFi... (Espero)
}
void TareaB() {
take(mutWiFi); // Tengo el WiFi
// ...
take(mutSD); // Intento pillar la SD... (Espero a Tarea A)
}- A coge la SD.
- El Scheduler cambia a B.
- B coge el WiFi.
- B intenta coger la SD. Está ocupada por A. B se bloquea.
- El Scheduler vuelve a A.
- A intenta coger el WiFi. Está ocupado por B. A se bloquea.
Resultado: A espera a B, y B espera a A. Ambas tareas se quedan en estado Blocked eternamente. El resto del sistema sigue funcionando (si hay otras tareas), pero la funcionalidad de WiFi y SD ha muerto.
Cómo evitar el Deadlock
El Deadlock es un error de diseño lógico. No lo arregla el compilador ni el sistema operativo (FreeRTOS no detecta Deadlocks automáticamente). Lo tenemos que arreglar nosotros con buenas prácticas.
Orden jerárquico
Establecer una regla de diseño estricta: “Los recursos siempre se toman en el mismo orden”.
Si decidimos que el orden es 1. WiFi, 2. SD:
- Tarea B: Toma WiFi Toma SD. (Correcto)
- Tarea A: Debería tomar WiFi Toma SD. (Aunque solo necesite la SD primero, si va a necesitar el WiFi luego, debe seguir el orden o cogerlos todos al principio).
Si ambas tareas intentan coger primero el WiFi, la segunda que llegue se bloqueará en el primer paso, pero no habrá abrazo mortal porque ninguna tiene todavía el recurso que la otra quiere.
Timeouts y retroceso
No usar portMAX_DELAY (espera infinita) en los Mutex.
if (xSemaphoreTake(mutSD, pdMS_TO_TICKS(100)) == pdTRUE) {
if (xSemaphoreTake(mutWiFi, pdMS_TO_TICKS(100)) != pdTRUE) {
// No pudimos coger el segundo recurso: soltamos el primero.
xSemaphoreGive(mutSD);
vTaskDelay(pdMS_TO_TICKS(20));
} else {
// Usar ambos recursos y liberarlos en orden inverso.
xSemaphoreGive(mutWiFi);
xSemaphoreGive(mutSD);
}
}Es similar a lo que hacemos los humanos cuando nos cruzamos en un pasillo estrecho: ambos nos apartamos, esperamos un segundo y uno pasa.
Un único propietario
Otra opción es que una sola tarea sea propietaria del recurso. Las demás le envían operaciones mediante una cola y reciben el resultado por otra cola o una notificación. Así desaparece la necesidad de tomar ese mutex desde varios lugares.
No intentes tomar un mutex dentro de una sección crítica. Tomar un mutex puede bloquear la tarea, algo incompatible con haber suspendido la planificación o enmascarado interrupciones.
Checklist de diagnóstico
Cuando el sistema se cuelgue o responda tarde, revisa esta lista:
- Starvation: ¿Tienes una tarea de prioridad alta en un
while(1)sinvTaskDelay? - Deadlock: ¿Tienes dos tareas esperando recursos cruzados? (Revisa el orden de los
Take). - Priority Inversion: ¿Estás usando Semáforos Binarios para proteger variables? (Cámbialos por Mutex).
- Race Condition: ¿Dos tareas escriben en la misma variable sin protección?
- Stack Overflow: ¿Has calculado bien la memoria de la tarea? (Usa
uxTaskGetStackHighWaterMark). - Interrupt Storm: ¿Tu interrupción salta demasiado rápido y satura la CPU?
- Unprotected Reentrancy: ¿Llamas a una función no segura (
strtok,printfen algunas implementaciones) desde dos tareas a la vez?