Módulo 1.7

Sincronización entre Threads

Coordinación segura de acceso a recursos compartidos

Santi Scagliusi, PhD

Comenzar arrow_downward
warning
Race Conditions
Condiciones de carrera
flag
Semáforos
Conteo de recursos
lock
Mutexes
Exclusión mutua
mail
Message Queues
Colas de mensajes

¿Por qué necesitamos sincronización?

Cuando múltiples threads acceden a los mismos datos, pueden ocurrir problemas.

error

El problema

  • close Múltiples threads acceden al mismo código o datos
  • close El scheduler puede interrumpir en cualquier momento
  • close Los datos quedan en estado inconsistente
  • close Comportamiento impredecible y difícil de depurar

Sección crítica: código que accede a recursos compartidos y debe ejecutarse de forma atómica.

check_circle

La solución

  • check Mecanismos que garantizan acceso exclusivo
  • check Solo un thread ejecuta la sección crítica a la vez
  • check Otros threads esperan hasta que el recurso esté libre
  • check Comportamiento predecible y correcto

Primitivas de sincronización: semáforos, mutexes y colas de mensajes.

Escenario típico: contador compartido

Thread A: incrementar
count = read(count); // Lee 5
count = count + 1; // Calcula 6
write(count); // Escribe 6
Thread B: incrementar
count = read(count); // Lee 5 (!)
count = count + 1; // Calcula 6
write(count); // Escribe 6
warning Resultado: count = 6 en lugar de 7

Mutex en Acción: Antes y Después

Avanza paso a paso y observa cómo la misma secuencia de operaciones produce resultados distintos con y sin mutex.

Variable compartida
counter
Valor
5
Esperado
7
Thread A
Thread B
0
Pulsa "Siguiente paso" para comenzar la simulación.
Paso 0 / 0
school
Momento clave: sin mutex, ambos threads leen el mismo valor (5) antes de escribir, causando que una actualización se pierda. Con mutex, el acceso es serializado y el resultado es correcto.

Sección Crítica y Condiciones de Carrera

Ejemplo práctico: dos contadores que siempre deben sumar 40.

race_condition.c
// Invariante: inc_count + dec_count = 40
int inc_count = 20;
int dec_count = 20;
void shared_code(void) {
// Sección crítica
inc_count += 1;
// Preempción aquí = problema
dec_count -= 1;
}
warning
Problema: si el scheduler interrumpe entre las dos operaciones, otro thread puede ver datos inconsistentes.

timeline Línea temporal de la corrupción

1
Estado inicial
inc=20, dec=20, suma=40
2
Thread A: inc_count += 1
inc=21, dec=20, suma=41
!
PREEMPCIÓN - Thread B ejecuta
Thread A interrumpido
3
Thread B: lee inc=21, dec=20
Ve suma=41 (inconsistente)
4
Thread A: dec_count -= 1
inc=21, dec=19, suma=40
bug_report
Race condition: el resultado depende del orden de ejecución de los threads, que es impredecible.

Semáforos en Zephyr

Mecanismo de conteo para gestionar acceso a recursos finitos.

Concepto

Un semáforo mantiene un contador interno que representa el número de recursos disponibles.

10
Disponible
arrow_forward
5
Parcial
arrow_forward
0
Bloqueado

Propiedades del semáforo

  • settings Cuenta inicial y límite al definir
  • add k_sem_give(): incrementa contador (thread o ISR)
  • remove k_sem_take(): decrementa, bloquea si es 0
  • group Sin propiedad: cualquier thread puede give/take
  • priority_high Sin herencia de prioridad
info
Desde ISR: k_sem_give() siempre permitido. k_sem_take() solo con K_NO_WAIT (nunca bloquear en ISR).

Ejemplo de Código

Patrón productor-consumidor con semáforos.

semaphore_example.c
// Definir semáforo con 10 recursos
K_SEM_DEFINE(resource_sem, 10, 10);
void consumer(void) {
// Espera recurso (bloquea si count = 0)
k_sem_take(&resource_sem, K_FOREVER);
// Usar recurso...
use_resource();
// Liberar recurso
k_sem_give(&resource_sem);
}
producer.c
void producer(void) {
// Crear nuevo recurso
create_resource();
// Señalar disponibilidad
k_sem_give(&resource_sem);
}
Semáforo "lleno"
initial = limit

Limitar acceso concurrente

Semáforo "vacío"
initial=0, limit=1

Señalización/gate

Mutexes en Zephyr

Exclusión mutua para proteger secciones críticas.

Concepto

Un mutex (Mutual Exclusion) es un candado binario: bloqueado o desbloqueado.

lock_open Desbloqueado
Disponible
swap_horiz
lock Bloqueado
En uso

Propiedades del mutex

  • person Propiedad: solo el dueño puede desbloquear
  • lock k_mutex_lock(): adquirir, bloquea si ocupado
  • lock_open k_mutex_unlock(): liberar (solo dueño)
  • replay Recursivo: mismo thread puede lock varias veces
  • priority_high Herencia de prioridad: evita inversión
warning
Solo desde threads: los mutexes NO pueden usarse desde ISRs.

Ejemplo de Código

Protegiendo una sección crítica con mutex.

mutex_example.c
// Definir mutex
K_MUTEX_DEFINE(data_mutex);
// Variables protegidas
int increment_count = 20;
int decrement_count = 20;
void shared_code_section(void) {
k_mutex_lock(&data_mutex, K_FOREVER);
increment_count += 1;
decrement_count -= 1;
k_mutex_unlock(&data_mutex);
}
lightbulb
Bloqueo recursivo: si el mismo thread llama k_mutex_lock() mientras ya tiene el mutex, incrementa un contador interno y no se bloquea.
check_circle
Invariante protegido: ahora increment_count + decrement_count = 40 siempre se mantiene visible para otros threads.

Semáforos vs Mutexes

Diferencias clave entre ambas primitivas.

Aspecto
flag Semáforo
lock Mutex
Uso típico Contar recursos disponibles Proteger sección crítica
Propiedad cualquiera puede give/take solo dueño unlock
Desde ISR give siempre, take con K_NO_WAIT nunca
Herencia de prioridad
Valor inicial 0 a límite (configurable) Siempre desbloqueado
Bloqueo recursivo

¿Cuál usar?

Guía rápida para elegir la primitiva correcta.

flag Usa semáforo cuando...

  • check Necesitas contar recursos (N conexiones, buffers...)
  • check Un thread produce, otro consume (señalización)
  • check Necesitas señalizar desde una ISR

lock Usa mutex cuando...

  • check Proteges datos compartidos (sección crítica)
  • check El mismo thread que bloquea debe desbloquear
  • check Necesitas herencia de prioridad
tips_and_updates
Regla general: si estás protegiendo datos compartidos, usa mutex. Si estás contando o señalizando, usa semáforo.

Herencia de Prioridad

¿Cómo evitar que threads de baja prioridad bloqueen a los de alta?

warning El problema: Inversión de prioridad

Alta Thread A quiere el mutex
Media Thread B ejecutando
Baja Thread C tiene el mutex
Resultado: Thread A (alta prioridad) espera a C (baja), pero B (media) le quita CPU a C. A queda bloqueado indefinidamente.

check_circle La solución: Herencia de prioridad

Alta Thread A quiere el mutex
Media Thread B esperando
Alta* Thread C hereda prioridad
Resultado: C hereda la prioridad de A temporalmente. Puede terminar sin ser interrumpido por B.
tips_and_updates
Automático en Zephyr: la herencia de prioridad está implementada en los mutexes. Los semáforos NO la tienen.

Colas de Mensajes

Alternativa thread-safe para pasar datos entre threads.

Concepto

Una cola FIFO para enviar mensajes de tamaño fijo entre threads. Evita la necesidad de variables compartidas.

Producer
arrow_forward
msg1
msg2
msg3
arrow_forward
Consumer

Operaciones principales

  • send k_msgq_put(): añadir mensaje a la cola
  • inbox k_msgq_get(): recibir mensaje de la cola
  • hourglass_empty Bloquea emisor si cola llena
  • hourglass_empty Bloquea receptor si cola vacía
tips_and_updates
Ventaja: los datos se copian a la cola. No hay variables compartidas, no hay race conditions.

Ejemplo de Código

Definición y uso de una cola de mensajes para datos de sensor.

message_queue.c
// Estructura del mensaje
struct sensor_data {
int temp;
int humidity;
};
// Cola de 10 mensajes
K_MSGQ_DEFINE(my_msgq,
sizeof(struct sensor_data),
10, // capacidad
4); // alineación
producer_consumer.c
// Productor
struct sensor_data data = {
.temp = 25,
.humidity = 60
};
k_msgq_put(&my_msgq, &data, K_FOREVER);
// Consumidor
struct sensor_data received;
k_msgq_get(&my_msgq, &received, K_FOREVER);

Buenas Prácticas de Sincronización

Consejos para evitar problemas comunes.

timer

Secciones cortas

Minimiza el tiempo dentro de la sección crítica. Otros threads están esperando.

lock_open

Siempre liberar

Libera los locks incluso en paths de error. Usa patrones como goto cleanup.

block

Evitar locks anidados

Múltiples locks aumentan el riesgo de deadlock. Si es necesario, siempre en el mismo orden.

lock

Mutex para datos

Usa mutex cuando proteges estructuras de datos compartidas.

flag

Semáforo para conteo

Usa semáforo para contar recursos o señalizar eventos.

mail

Message queue para datos

Usa colas de mensajes para transferir datos entre threads.

Evitando Deadlocks

Un deadlock ocurre cuando dos threads esperan mutuamente por recursos que el otro tiene.

Incorrecto - Deadlock
// Thread A
k_mutex_lock(&mutex_1, ...);
k_mutex_lock(&mutex_2, ...); // Espera
// Thread B
k_mutex_lock(&mutex_2, ...);
k_mutex_lock(&mutex_1, ...); // Espera
Correcto - Mismo orden
// Thread A - Mismo orden
k_mutex_lock(&mutex_1, ...);
k_mutex_lock(&mutex_2, ...);
// Thread B - Mismo orden
k_mutex_lock(&mutex_1, ...);
k_mutex_lock(&mutex_2, ...);
lightbulb
Regla de oro: si necesitas adquirir múltiples locks, siempre hazlo en el mismo orden global en todos los threads.

Puntos Clave

Conceptos fundamentales de sincronización en Zephyr.

warning

Race Conditions

Ocurren cuando threads acceden a datos compartidos sin sincronización

flag

Semáforos

Conteo de recursos. Sin propiedad. Usables desde ISR

lock

Mutexes

Exclusión mutua. Con propiedad y herencia de prioridad

mail

Message Queues

Paso de datos thread-safe. Evita variables compartidas

priority_high

Herencia de prioridad

Evita inversión de prioridad. Solo en mutexes

timer

Secciones cortas

Minimiza tiempo en sección crítica. Siempre libera locks

tips_and_updates

"La sincronización correcta es esencial para sistemas multithreading fiables."

Elige la primitiva adecuada: mutex para proteger datos, semáforo para contar, cola para transferir.

arrow_forward Siguiente módulo

2.1 Zephyr RTOS: Más allá de los básicos

Exploraremos características avanzadas de Zephyr: timers, eventos, y gestión de energía.