← Concurrencia y Sincronización Bloque 6 · Variables de condición y monitores · Tema 26 de 47

Tema 26

Despertares espurios y el bucle while

Los despertares espurios (spurious wakeups) son uno de los aspectos más sutiles de las variables de condición: un hilo puede despertar de pthread_cond_wait sin que nadie haya llamado a signal o broadcast. Por eso, toda espera sobre una condición debe ir dentro de un while, no de un if. En este tema verás por qué ocurren los despertares espurios, cómo el bucle while los maneja, y la diferencia entre signal y broadcast.

1. Qué es un despertar espurio

Un despertar espurio (spurious wakeup) es cuando un hilo bloqueado en pthread_cond_wait se despierta sin que nadie haya llamado a signal o broadcast. El hilo retorna de wait como si alguien lo hubiera señalado, pero la condición puede no cumplirse.

Los despertares espurios son permitidos por POSIX: la especificación dice que pthread_cond_wait puede retornar en cualquier momento, incluso sin señal. Esto es intencional: permite implementaciones más eficientes del sistema operativo.

Los despertares espurios son raros en la práctica, pero ocurren. Un programa que asume que wait solo retorna cuando alguien señaló tendrá bugs intermitentes.

Los despertares espurios son reales

Aunque son raros, los despertares espurios ocurren en sistemas reales. Un programa que no los maneja tendrá bugs que aparecen solo bajo carga y son difíciles de reproducir.

2. Por qué ocurren

Los despertares espurios ocurren por razones de implementación:

La especificación de POSIX permite los despertares espurios para dar flexibilidad a la implementación. Esto simplifica el código del núcleo pero complica el código del usuario.

3. if vs. while

La diferencia entre usar if y while para verificar la condición es la clave para manejar los despertares espurios:

PatrónCódigoComportamiento
if (incorrecto)if (!cond) wait();Si el hilo se despierta espuriamente, sale del if y continúa sin verificar la condición.
while (correcto)while (!cond) wait();Si el hilo se despierta espuriamente, vuelve a verificar la condición. Si no se cumple, vuelve a esperar.

El while garantiza que la condición se verifica cada vez que wait retorna, incluso si el despertar fue espurio. El if no lo garantiza.

4. El problema con if

Usar if en lugar de while causa bugs sutiles:

if (incorrecto)
pthread_mutex_lock(&mutex);
if (!dato_listo) {
    pthread_cond_wait(&cond, &mutex);  /* puede despertar espuriamente */
}
/* Si se despertó espuriamente, dato_listo es false. */
printf("%d\n", dato);  /* BUG: puede imprimir basura */
pthread_mutex_unlock(&mutex);

Si el hilo se despierta espuriamente, dato_listo es false y dato no está listo. El hilo imprime un valor incorrecto.

El problema es que el if solo verifica la condición una vez. Si el despertar es espurio, la condición no se cumple, pero el hilo continúa de todos modos.

5. La solución con while

Usar while resuelve el problema:

while (correcto)
pthread_mutex_lock(&mutex);
while (!dato_listo) {
    pthread_cond_wait(&cond, &mutex);  /* puede despertar espuriamente */
    /* Al despertar, vuelve a verificar la condición. */
}
/* condición cumplida */
printf("%d\n", dato);  /* seguro: dato está listo */
pthread_mutex_unlock(&mutex);

Si el hilo se despierta espuriamente, el while vuelve a verificar !dato_listo. Si es true (no listo), vuelve a esperar. Si es false (listo), sale del bucle y continúa.

El while garantiza que la condición se cumple cuando el hilo sale del bucle, independientemente de si el despertar fue espurio o no.

La regla de oro

Toda espera sobre una condición debe ir dentro de un while, nunca de un if. Esta es la regla más importante de las variables de condición.

6. signal vs. broadcast

La diferencia entre signal y broadcast es fundamental:

Aspectosignalbroadcast
Cuántos despiertaUno.Todos.
Cuándo usarCuando la condición hace que un solo hilo pueda continuar.Cuando la condición hace que varios hilos puedan continuar.
CostoBajo (un solo hilo).Alto (todos los hilos).
RiesgoPuede despertar al hilo equivocado.No hay riesgo de olvidar un hilo.

El problema con signal es que puede despertar al hilo equivocado. Si la condición hace que solo un hilo pueda continuar, pero signal despierta a otro, el hilo despertado vuelve a esperar (porque la condición no se cumple para él) y el hilo correcto sigue bloqueado.

Este problema se llama despertar perdido (lost wakeup): el hilo correcto nunca se despierta porque signal despertó a otro.

7. Cuándo usar signal

signal se usa cuando la condición hace que un solo hilo pueda continuar:

La regla es: si la condición hace que solo un hilo pueda continuar, usa signal. Si puede hacer que varios continúen, usa broadcast.

8. Cuándo usar broadcast

broadcast se usa cuando la condición hace que varios hilos puedan continuar:

broadcast es más costoso que signal porque despierta a todos los hilos. Pero es necesario cuando la condición afecta a varios hilos.

signal es suficiente en la mayoría de los casos

En la mayoría de los casos, signal es suficiente. broadcast se usa solo cuando la condición hace que varios hilos puedan continuar. Usar broadcast cuando signal bastaría es un desperdicio de CPU.

9. Semántica de Hoare vs. Mesa

Hay dos semánticas para las variables de condición:

SemánticaQué ocurre al hacer signalVentajaDesventaja
HoareEl hilo señalador se bloquea y el hilo señalado se ejecuta inmediatamente (con el mutex).La condición se cumple cuando el hilo señalado se ejecuta.Más cambios de contexto.
MesaEl hilo señalador continúa ejecutándose. El hilo señalado se despierta pero debe esperar el mutex.Menos cambios de contexto.La condición puede no cumplirse cuando el hilo señalado se ejecuta (por eso se necesita while).

POSIX usa la semántica de Mesa: signal solo despierta al hilo, pero no le transfiere el mutex inmediatamente. El hilo despertado debe esperar a que el mutex se libere.

La semántica de Mesa es la razón por la que se necesita el while: cuando el hilo despertado finalmente adquiere el mutex, la condición puede haber cambiado (otro hilo la consumió). El while vuelve a verificar.

Con la semántica de Hoare, el if sería suficiente (porque la condición se cumple cuando el hilo se ejecuta). Pero Hoare es más costosa y no se usa en POSIX.

10. Un ejemplo desarrollado

Un ejemplo que muestra el problema del despertar espurio y cómo el while lo resuelve:

despertar_espurio.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int dato = 0;
int dato_listo = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *productor(void *arg) {
    for (int i = 0; i < 3; i++) {
        sleep(1);
        pthread_mutex_lock(&mutex);
        dato = i;
        dato_listo = 1;
        printf("Productor: produjo %d\n", dato);
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void *consumidor(void *arg) {
    for (int i = 0; i < 3; i++) {
        pthread_mutex_lock(&mutex);
        /* Usar while, no if. */
        while (!dato_listo) {
            pthread_cond_wait(&cond, &mutex);
        }
        printf("Consumidor: consumió %d\n", dato);
        dato_listo = 0;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main(void) {
    pthread_t prod, cons;
    pthread_create(&prod, NULL, productor, NULL);
    pthread_create(&cons, NULL, consumidor, NULL);
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);
    return 0;
}

Con while, el consumidor verifica dato_listo cada vez que se despierta. Si el despertar es espurio, dato_listo es 0 y vuelve a esperar. Si es una señal real, dato_listo es 1 y sale del bucle.

11. Simulador interactivo

Este simulador muestra el problema del despertar espurio y cómo el while lo resuelve:

Simulador · despertares espurios

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es un despertar espurio?

Un hilo se despierta de pthread_cond_wait sin que nadie haya llamado a signal o broadcast.

2. ¿Por qué ocurren los despertares espurios?

Por optimización del núcleo, señales del sistema e implementación de futex.

3. ¿Por qué se usa while en lugar de if?

Porque el while vuelve a verificar la condición después de un despertar espurio.

4. ¿Qué ocurre si se usa if y hay un despertar espurio?

El hilo sale del if y continúa sin verificar la condición, causando un bug.

5. ¿Cuál es la diferencia entre signal y broadcast?

signal despierta a un hilo; broadcast despierta a todos.

6. ¿Cuándo se usa signal?

Cuando la condición hace que un solo hilo pueda continuar.

7. ¿Cuándo se usa broadcast?

Cuando la condición hace que varios hilos puedan continuar.

8. ¿Qué es la semántica de Mesa?

signal solo despierta al hilo, que debe esperar el mutex. No transfiere el mutex inmediatamente.

9. ¿Qué es la semántica de Hoare?

signal transfiere el mutex al hilo despertado inmediatamente.

10. ¿Por qué POSIX usa la semántica de Mesa?

Porque es más eficiente (menos cambios de contexto).

Para practicar

  1. Escribe un programa que use if en lugar de while y observa el bug.
  2. Corrige el programa usando while.
  3. Usa broadcast en lugar de signal y observa la diferencia.
  4. Investiga cómo se implementan los despertares espurios en Linux.
  5. Explica por qué la semántica de Mesa requiere el while.
  6. Investiga la diferencia de rendimiento entre signal y broadcast.