Cuando escribes dinero = dinero + 1; en lenguaje C, asumes que la acción ocurre en un solo paso instantáneo. Pero el compilador traduce esa línea a 3 instrucciones en lenguaje Ensamblador (Assembly) para el procesador ARM Cortex:
Una Condición de Carrera ocurre cuando dos tareas comparten la misma variable global. Imagina que la Tarea A extrae el valor `500` a su registro y le suma 1 (ahora tiene `501` en su cabeza). Pero justo antes de ejecutar la instrucción STR (Store) para guardarlo, ¡PUM! El Tick de FreeRTOS dispara un Cambio de Contexto.
La Tarea B entra a la CPU. Lee la memoria RAM original, la cual sigue valiendo `500`. Le suma 1 (`501`) y la guarda en la RAM. Luego, el RTOS le devuelve el control a la Tarea A. Esta despierta, recuerda que en su registro tenía el número `501` y lo guarda en la RAM.
¡El desastre ocurrió! Ambas tareas sumaron 1 (el resultado debió ser `502`), pero la RAM terminó valiendo `501`. Un dato entero ha sido borrado del universo.
Ejercicio 1: Vamos a escribir un código malicioso (anti-patrón) para probar este concepto empíricamente en nuestro simulador. Crearemos una variable global dinero_total. Crearemos dos Hilos (Manguera A y Manguera B). Ambos intentarán sumarle 1 peso a la variable de manera simultánea 1,000,000 de veces. Matemáticamente, el resultado debería ser $2,000,000. Pero debido a la Condición de Carrera, veremos cómo el dinero se desvanece en el aire.
/**
* @file race_condition.c
* @brief Demostración destructiva de acceso concurrente no protegido (Data Tearing)
*/
#include "stm32f401xe.h"
#include "cmsis_os2.h"
#include <stdio.h>
// =======================================================
// EL RECURSO COMPARTIDO (Vulnerable)
// =======================================================
// 'volatile' evita que el compilador optimice la variable en un registro,
// forzando siempre la lectura/escritura a la memoria RAM.
volatile uint32_t dinero_total = 0;
// Cantidad de incrementos que hará cada manguera
#define CICLOS_DE_VENTA 1000000
// =======================================================
// HILO 1: MANGUERA A
// =======================================================
void Thread_MangueraA(void *argument) {
for (uint32_t i = 0; i < CICLOS_DE_VENTA; i++) {
// ¡ZONA DE PELIGRO!
// Esta línea es un Read-Modify-Write.
// Si el Kernel hace preempción aquí a la mitad, el dato se corrompe.
dinero_total++;
}
// Hilo terminado, se suspende
osThreadSuspend(osThreadGetId());
}
// =======================================================
// HILO 2: MANGUERA B
// =======================================================
void Thread_MangueraB(void *argument) {
for (uint32_t i = 0; i < CICLOS_DE_VENTA; i++) {
// La misma variable, el mismo peligro.
dinero_total++;
}
// Hilo terminado, se suspende
osThreadSuspend(osThreadGetId());
}
// =======================================================
// INICIALIZACIÓN
// =======================================================
int main(void) {
SystemClock_Config();
osKernelInitialize();
// ATRIBUTOS: A propósito, les daremos EXACTAMENTE la misma prioridad.
// Esto forzará al Kernel de FreeRTOS a hacer "Round-Robin" (turnarlos a la fuerza)
// cada 1 milisegundo (Tick), incrementando brutalmente la probabilidad de chocar.
const osThreadAttr_t attr_mangueras = {
.stack_size = 512,
.priority = osPriorityNormal,
};
osThreadNew(Thread_MangueraA, NULL, &attr_mangueras);
osThreadNew(Thread_MangueraB, NULL, &attr_mangueras);
osKernelStart();
// RESULTADO EMPÍRICO ESPERADO EN PRODUCCIÓN:
// Si imprimes 'dinero_total' al final, NO será 2,000,000.
// Terminará siendo un número impredecible y errático como 1,345,821.
// Has perdido cientos de miles de pesos debido a la sobreescritura de contexto.
while (1);
}
Objetivo del día: Identificación Temprana de Fallas de Arquitectura.
El peor error que puede tener un software no es uno que hace que la máquina se reinicie (eso es fácil de notar y arreglar). El peor error es un Fallo Silencioso. Una Condición de Carrera no lanza pantallas azules, no activa alarmas de hardware ni arroja errores de compilador. Simplemente, los datos se desvanecen.
Imagínate que tu Despachador de Gas LP atiende a dos vehículos mediante un modelo de doble manguera. El Hilo de la Manguera A procesa $15.00 MXN en gas, y en el exacto instante en que está sumando el dinero al `Corte_de_Caja_Total`, el Hilo de la Manguera B lo interrumpe y suma $20.00 MXN. Si ocurre la Condición de Carrera, la RAM sobrescribirá el total y desaparecerán $15.00 de la contabilidad de la gasolinera. Esto te metería en problemas legales severos de metrología. Hoy aprendiste a diagnosticar la enfermedad; mañana (Día 189) inventaremos la cura usando candados criptográficos llamados Mutexes.
++, +=, o reasignaciones lógicas) no son **Atómicas**. Se dividen a nivel de Ensamblador en tres fases vulnerables: Leer de RAM a Registro (Load), Modificar el Registro (ALU), y Escribir de Registro a RAM (Store).