La sincronización es la coordinación del acceso a datos, periféricos y eventos compartidos entre varios contextos de ejecución.
Imaginad que dos hilos intentan escribir en la misma variable global, o enviar datos por el mismo puerto I2C al mismo tiempo. ¿Quién gana? ¿Se mezclan los datos?
Lo que ocurre es un desastre conocido como Condición de Carrera (Race Condition). Los datos se corrompen y el programa falla de forma aleatoria y difícil de depurar.
Para evitar esto, Zephyr nos proporciona mecanismos de sincronización. Hoy vamos a ver los dos más importantes: el Mutex y el Semáforo.
El problema: la condición de carrera
Vamos a visualizar el problema con un ejemplo clásico. Imaginad una variable global contador = 0.
- El Hilo A quiere incrementar el contador (
contador++). - El Hilo B quiere incrementar el contador (
contador++).
Si ambos lo hacen a la vez, podríamos pensar que el resultado será 2. Pero en ensamblador, contador++ son tres pasos:
Leer valor de memoria al registro CPU.
Sumar 1 al registro.
Escribir registro en memoria.
Si el Hilo A es interrumpido por el Hilo B justo después del paso 1, ambos hilos leerán que el contador vale 0. Ambos sumarán 1, y ambos escribirán 1. Resultado: 1 (en lugar de 2). Hemos perdido datos.
Cualquier recurso compartido (variables globales, buffers, periféricos de hardware como UART/I2C) debe ser protegido si más de un hilo va a acceder a él.
La solución: mutex (exclusión mutua)
Un Mutex (Mutual Exclusion) funciona como la llave del baño de una gasolinera. Solo hay una llave.
- Si quieres entrar, pides la llave (
lock). - Si no está, te esperas en la puerta a que salga el que está dentro.
- Cuando acabas, devuelves la llave (
unlock) para que pase el siguiente.
En Zephyr, un Mutex asegura que solo un hilo pueda acceder a una sección de código crítica a la vez.
Implementación en Zephyr
Vamos a proteger nuestro contador usando un Mutex.
#include <zephyr/kernel.h>
/* 1. Definimos el Mutex estáticamente */
K_MUTEX_DEFINE(my_mutex);
int contador_compartido = 0;
void incrementar_contador(void)
{
/* 2. Intentamos coger el Mutex */
/* K_FOREVER significa: "si está ocupado, duérmeme hasta que esté libre" */
k_mutex_lock(&my_mutex, K_FOREVER);
/* --- INICIO SECCIÓN CRÍTICA --- */
/* Aquí dentro estamos solos. Nadie más puede entrar. */
contador_compartido++;
printk("Contador: %d\n", contador_compartido);
/* --- FIN SECCIÓN CRÍTICA --- */
/* 3. Soltamos el Mutex (¡OBLIGATORIO!) */
k_mutex_unlock(&my_mutex);
}Cuidado con los deadlocks. Si un hilo conserva un mutex o varios hilos los adquieren en distinto orden, los hilos implicados pueden quedar esperando indefinidamente. Libera el mutex en todas las rutas de salida y mantén un orden de adquisición estable.
Mutex reentrante
Una característica genial de los Mutex en Zephyr es que son reentrantes. Si el hilo que ya tiene la llave intenta cogerla otra vez, el sistema le deja pasar (simplemente incrementa un contador interno). Esto evita que un hilo se bloquee a sí mismo por error.
Semáforos (k_sem)
Si el Mutex es una llave, el Semáforo es un contador de plazas libres en un parking. O un mecanismo de notificación.
A diferencia del Mutex, el Semáforo no tiene dueño. Un hilo puede cogerlo (decrementar) y otro distinto puede soltarlo (incrementar). Esto lo hace ideal para sincronizar eventos (ej. “Ha llegado un dato”).
Caso de uso: productor-consumidor
Imaginad un Timer (que aprendimos en el artículo anterior) que lee un sensor cada segundo. Como el Timer no puede procesar datos complejos, avisa a un Hilo para que lo haga.
#include <zephyr/kernel.h>
/* Definimos un semáforo con valor inicial 0 y valor máximo 1 */
K_SEM_DEFINE(my_sem, 0, 1);
/* Timer: El Productor */
void my_timer_handler(struct k_timer *dummy)
{
/* "Doy" el semáforo (Incremento su valor) */
/* Esto avisa a quien esté esperando */
k_sem_give(&my_sem);
}
/* Hilo: El Consumidor */
void processing_thread(void *p1, void *p2, void *p3)
{
while (1) {
/* "Tomo" el semáforo. */
/* Si el valor es 0, el hilo se duerme aquí hasta que el timer haga 'give' */
k_sem_take(&my_sem, K_FOREVER);
/* Si llegamos aquí, es que el timer ha saltado */
printk("¡Evento recibido! Procesando datos...\n");
}
}En este ejemplo, el hilo processing_thread no gasta CPU. Está dormido (Suspended) hasta que el semáforo se pone en verde. Es la forma más eficiente de coordinar tareas.
Mutex vs semáforo
Es fácil confundirlos. La diferencia práctica es esta:
| Característica | Mutex (k_mutex) | Semáforo (k_sem) |
|---|---|---|
| Objetivo | Proteger recursos (Exclusión) | Señalizar eventos / Limitar acceso |
| Concepto | Llave única | Contador de recursos |
| Dueño | Sí (quien cierra debe abrir) | No (cualquiera puede abrir/cerrar) |
| Uso típico | Acceso a variable global, I2C, UART | Interrupción avisa a Hilo, Sincronización |
| Prioridad | Soporta Herencia de Prioridad | No altera prioridades |
Herencia de Prioridad: Zephyr es listo. Si un hilo de Alta Prioridad está esperando un Mutex que tiene un hilo de Baja Prioridad, Zephyr sube temporalmente la prioridad del hilo bajo para que acabe rápido y suelte el Mutex. Esto evita la temida “Inversión de Prioridad”.