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:
- Espera activa: bloquear el mutex, verificar la condición, desbloquear, repetir. Esto gasta CPU.
- Dormir: bloquear el mutex, verificar, desbloquear, dormir un tiempo, repetir. Esto es ineficiente y tiene latencia.
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.
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ón | Descripció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:
- Libera el mutex. El hilo deja de tener el lock. liberar
- Se bloquea. El hilo se duerme y cede la CPU. bloquear
- 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.
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).
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:
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:
- Se añade a la cola: el hilo se añade a la cola de la condición. encolar
- Se libera el mutex: el hilo libera el mutex para que otros hilos puedan modificar los datos. liberar
- Se bloquea: el hilo se duerme y cede la CPU. bloquear
- Al despertar: el hilo se quita de la cola y re-adquiere el mutex. re-adquirir
Cuando un hilo llama a pthread_cond_signal:
- Se despierta a un hilo: se saca el primer hilo de la cola y se marca como «despierto». despertar
- 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:
#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:
/* 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:
- Siempre bloquear el mutex antes de verificar la condición.
- Siempre verificar la condición en un while, no en un if. (Se explica en el tema 26.)
- Siempre señalar dentro del mutex. (O inmediatamente después de liberarlo.)
- Siempre liberar el mutex después.
10. Propiedades
| Propiedad | ¿La cumple? | Comentario |
|---|---|---|
| Exclusión mutua | Sí (con mutex) | El mutex protege los datos compartidos. |
| Espera eficiente | Sí | El hilo se bloquea y cede la CPU. |
| Señalización | Sí | signal despierta a un hilo, broadcast a todos. |
| Reentrancia | Sí | Se puede llamar desde cualquier hilo. |
| Sin estado propio | Sí | 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
- La variable de condición permite esperar a que se cumpla una condición sin gastar CPU.
- Siempre se usa con un mutex que protege los datos compartidos.
- pthread_cond_wait libera el mutex, bloquea y re-adquiere el mutex al despertar.
- pthread_cond_signal despierta a un hilo; broadcast despierta a todos.
- La condición se verifica en un while, no en un if.
- La variable de condición no tiene estado propio: el estado está en los datos compartidos.
- Señalar dentro del mutex es la práctica recomendada.
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
- Implementa el productor-consumidor con variables de condición.
- Usa pthread_cond_timedwait para esperar con timeout.
- Compara signal con broadcast para una condición que afecta a todos.
- Investiga por qué se verifica la condición en un while (tema 26).
- Implementa un monitor usando mutex y variables de condición.
- Explica por qué signal fuera del mutex puede causar problemas.