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

Tema 25

Variables de condición

Los mutex protegen datos compartidos, pero no permiten esperar a que se cumpla una condición. Una variable de condición (condition variable) permite a un hilo esperar a que se cumpla una condición sin gastar CPU, y a otro hilo señalar cuando la condición se cumple. En este tema verás cómo funcionan pthread_cond_wait y pthread_cond_signal, y por qué son la herramienta ideal para coordinar hilos.

1. El problema: esperar una condición

Los mutex protegen datos compartidos, pero no permiten esperar a que se cumpla una condición. Por ejemplo, un hilo consumidor necesita esperar a que haya datos en un buffer antes de procesarlos. Con un mutex solo, el hilo tendría que:

La variable de condición resuelve este problema: permite a un hilo esperar eficientemente a que se cumpla una condición, sin gastar CPU, y a otro hilo señalar cuando la condición se cumple.

Condición + mutex = sincronización eficiente

La variable de condición siempre se usa con un mutex. El mutex protege los datos compartidos; la condición permite esperar eficientemente a que se cumpla una condición sobre esos datos.

2. Qué es una variable de condición

Una variable de condición (condition variable) es un mecanismo de sincronización que permite a un hilo esperar a que se cumpla una condición, y a otro hilo señalar cuando la condición se cumple. La espera es eficiente: el hilo se bloquea y cede la CPU, sin gastar ciclos.

La variable de condición no tiene estado propio (no tiene un contador como el semáforo). Su estado está implícito en los datos compartidos que protege el mutex. La variable de condición solo proporciona la mecánica de espera y señalización.

La analogía es un timbre: un hilo espera a que suene el timbre (wait), y otro hilo toca el timbre (signal) cuando ocurre un evento. El timbre no tiene estado: solo notifica.

3. La API de pthreads

FunciónDescripción
pthread_cond_init(cond, attr)Inicializa una variable de condición (o usar PTHREAD_COND_INITIALIZER).
pthread_cond_destroy(cond)Destruye una variable de condición.
pthread_cond_wait(cond, mutex)Espera a que se cumpla la condición. Libera el mutex y bloquea. Al despertar, re-adquiere el mutex.
pthread_cond_timedwait(cond, mutex, abstime)Espera con timeout (tiempo absoluto).
pthread_cond_signal(cond)Despierta a un hilo bloqueado en la condición.
pthread_cond_broadcast(cond)Despierta a todos los hilos bloqueados en la condición.

4. pthread_cond_wait

pthread_cond_wait es la operación central. Hace tres cosas de forma atómica:

  1. Libera el mutex. El hilo deja de tener el lock. liberar
  2. Se bloquea. El hilo se duerme y cede la CPU. bloquear
  3. Al despertar, re-adquiere el mutex. Antes de retornar, el hilo bloquea el mutex de nuevo. re-adquirir

La atomicidad de estas tres operaciones es crucial: si el mutex se liberara y el hilo se bloqueara en dos pasos separados, otro hilo podría hacer signal entre ellos y la señal se perdería.

cond_wait básico
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_mutex_lock(&mutex);
while (!condicion) {
    pthread_cond_wait(&cond, &mutex);  /* libera mutex, bloquea, re-adquiere */
}
/* condición cumplida, mutex bloqueado */
pthread_mutex_unlock(&mutex);

El while es esencial: se explica en el tema 26. Por ahora, basta con saber que pthread_cond_wait se llama dentro de un bucle que verifica la condición.

5. pthread_cond_signal

pthread_cond_signal despierta a un hilo bloqueado en la condición. Si hay varios hilos bloqueados, solo uno se despierta (el que el sistema elija, típicamente el primero en la cola).

signal
pthread_mutex_lock(&mutex);
/* Modificar los datos compartidos. */
dato = 42;
condicion = 1;
pthread_cond_signal(&cond);  /* despertar a un hilo */
pthread_mutex_unlock(&mutex);

signal no despierta al hilo inmediatamente: solo lo marca como «despierto». El hilo despertado debe esperar a que el mutex se libere antes de continuar.

Por eso, signal se llama típicamente dentro del mutex (entre lock y unlock). Si se llama fuera del mutex, el hilo despertado puede intentar adquirir el mutex y bloquearse de nuevo.

6. pthread_cond_broadcast

pthread_cond_broadcast despierta a todos los hilos bloqueados en la condición. Se usa cuando la condición puede hacer que varios hilos puedan continuar:

broadcast
pthread_mutex_lock(&mutex);
/* Modificar los datos compartidos. */
todos_listos = 1;
pthread_cond_broadcast(&cond);  /* despertar a todos */
pthread_mutex_unlock(&mutex);

broadcast es más costoso que signal porque despierta a todos los hilos. Se usa solo cuando es necesario (por ejemplo, cuando la condición afecta a todos los hilos).

7. Cómo funciona por dentro

Internamente, una variable de condición mantiene una cola de hilos bloqueados. Cuando un hilo llama a pthread_cond_wait:

  1. Se añade a la cola: el hilo se añade a la cola de la condición. encolar
  2. Se libera el mutex: el hilo libera el mutex para que otros hilos puedan modificar los datos. liberar
  3. Se bloquea: el hilo se duerme y cede la CPU. bloquear
  4. Al despertar: el hilo se quita de la cola y re-adquiere el mutex. re-adquirir

Cuando un hilo llama a pthread_cond_signal:

  1. Se despierta a un hilo: se saca el primer hilo de la cola y se marca como «despierto». despertar
  2. El hilo despertado espera el mutex: no continúa hasta que el mutex se libere. esperar

La implementación usa futexes (como los mutexes) para bloquear y despertar hilos eficientemente.

8. Un ejemplo desarrollado

Un ejemplo clásico: un hilo productor produce datos y un hilo consumidor los procesa. El consumidor espera a que haya datos:

productor_consumidor_cond.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 < 5; i++) {
        sleep(1);  /* simular producción */
        pthread_mutex_lock(&mutex);
        dato = i;
        dato_listo = 1;
        printf("Productor: produjo %d\n", dato);
        pthread_cond_signal(&cond);  /* señalar al consumidor */
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void *consumidor(void *arg) {
    for (int i = 0; i < 5; i++) {
        pthread_mutex_lock(&mutex);
        while (!dato_listo) {
            pthread_cond_wait(&cond, &mutex);  /* esperar datos */
        }
        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;
}

El consumidor bloquea en pthread_cond_wait hasta que el productor señale. Mientras espera, no consume CPU. Cuando el productor señala, el consumidor se despierta, re-adquiere el mutex y procesa el dato.

9. El patrón: mutex + condición

El patrón estándar para usar variables de condición es:

patrón estándar
/* Hilo que espera una condición. */
pthread_mutex_lock(&mutex);
while (!condicion) {              /* verificar la condición */
    pthread_cond_wait(&cond, &mutex);  /* esperar */
}
/* condición cumplida, mutex bloqueado */
/* ... usar los datos compartidos ... */
pthread_mutex_unlock(&mutex);

/* Hilo que señala la condición. */
pthread_mutex_lock(&mutex);
/* ... modificar los datos compartidos ... */
condicion = 1;
pthread_cond_signal(&cond);  /* o broadcast */
pthread_mutex_unlock(&mutex);

Las reglas del patrón son:

10. Propiedades

Propiedad¿La cumple?Comentario
Exclusión mutuaSí (con mutex)El mutex protege los datos compartidos.
Espera eficienteSíEl hilo se bloquea y cede la CPU.
SeñalizaciónSísignal despierta a un hilo, broadcast a todos.
ReentranciaSíSe puede llamar desde cualquier hilo.
Sin estado propioSíEl estado está en los datos compartidos.

11. Simulador interactivo

Este simulador muestra cómo una variable de condición coordina un productor y un consumidor:

Simulador · variable de condición

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es una variable de condición?

Un mecanismo para esperar a que se cumpla una condición sin gastar CPU.

2. ¿Con qué se usa siempre una variable de condición?

Con un mutex que protege los datos compartidos.

3. ¿Qué hace pthread_cond_wait?

Libera el mutex, bloquea el hilo, y re-adquiere el mutex al despertar.

4. ¿Por qué la liberación del mutex y el bloqueo son atómicos?

Para evitar que otro hilo haga signal entre ellos y la señal se pierda.

5. ¿Qué hace pthread_cond_signal?

Despierta a un hilo bloqueado en la condición.

6. ¿Qué hace pthread_cond_broadcast?

Despierta a todos los hilos bloqueados en la condición.

7. ¿Cuándo se usa broadcast en lugar de signal?

Cuando la condición puede hacer que varios hilos puedan continuar.

8. ¿Por qué se verifica la condición en un while?

Por los despertares espurios y la condición que puede no cumplirse al despertar.

9. ¿Dónde se debe llamar a signal?

Dentro del mutex (entre lock y unlock).

10. ¿Tiene estado propio la variable de condición?

No. El estado está en los datos compartidos protegidos por el mutex.

Para practicar

  1. Implementa el productor-consumidor con variables de condición.
  2. Usa pthread_cond_timedwait para esperar con timeout.
  3. Compara signal con broadcast para una condición que afecta a todos.
  4. Investiga por qué se verifica la condición en un while (tema 26).
  5. Implementa un monitor usando mutex y variables de condición.
  6. Explica por qué signal fuera del mutex puede causar problemas.