Sincronización de Hilos con Variables de Condición en C

Las variables de condición son mecanismos fundamentales para la coordinación entre hilos cuando el simple uso de mutex no es suficiente. A diferencia de los mutex, que controlan el acceso exclusivo a una sección crítica, las variables de condición permiten que uno o varios hilos esperen activamente hasta que se cumpla una condición lógica específica —por ejemplo, que un buffer no esté vacío o que tenga espacio disponible— y sean notificaods de forma eficeinte cuando dicha condición cambie.

¿Por qué usar variables de condición?

En escenarios como el patrón productor-consumidor, múltiples hilos productores insertan elementos en una estructura compartida (p. ej., una cola), mientras que múltiples hilos consumidores retiranlos. Un mutex por sí solo no resuelve el problema de espera activa ni permite notificar cambios de estado: si un consumidor encuentra la cola vacía, debe bloquearse sin consumir CPU innecesariamente; al mismo tiempo, cuando un productor añade un elemento, debe despertar a al menos un consumidor dormido. Las variables de condición ofrecen esta semántica de "esperar-hasta-que" de forma atómica y segura junto con un mutex.

API principal de pthread_cond

Declaración e inicialización

pthread_cond_t ready_queue = PTHREAD_COND_INITIALIZER;
pthread_mutex_t queue_lock = PTHREAD_MUTEX_INITIALIZER;

O bien, mediante funciones explícitas:

pthread_cond_t ready_queue;
pthread_mutex_t queue_lock;

pthread_cond_init(&ready_queue, NULL);
pthread_mutex_init(&queue_lock, NULL);

Bloqueo condicional atómico

La función pthread_cond_wait es clave: libera automáticamente el mutex proporcionado y pone el hilo en espera sobre la variable de condición. Al ser despertado (por signal o broadcast), adquiere nuevamente el mutex antes de retornar —todo ello como una operación atómica para evitar condiciones de carrera.

// Dentro del consumidor
pthread_mutex_lock(&queue_lock);
while (queue_is_empty()) {
    pthread_cond_wait(&ready_queue, &queue_lock); // Libera + espera + vuelve a bloquear
}
// Ahora se puede extraer seguro
item_t* item = dequeue();
pthread_mutex_unlock(&queue_lock);

Despertar hilos esperando

  • pthread_cond_signal(&ready_queue): despierta al menos un hilo bloqueado (eficiencia óptima cuando basta con uno).
  • pthread_cond_broadcast(&ready_queue): despierta todos los hilos bloqueados (útil cuando la condición afecta a varios, p. ej., finalización global).

Espera con timeout

Para evitar bloqueos indefinidos, se puede usar pthread_cond_timedwait, que toma un instante absoluto de expiración:

struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec += 5; // Espera 5 segundos como máximo

int result = pthread_cond_timedwait(&ready_queue, &queue_lock, &deadline);
if (result == ETIMEDOUT) {
    // Tratar el caso de timeout
}

Ejemplo práctico: Cola concurrente productor-consumidor

A continuación, una implementación robusta con 3 productores y 3 consumidores, usando una lista simplemente enlazada protegida por un mutex y una variable de condición para coordinar el acceso:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>

typedef struct node {
    int value;
    struct node* next;
} node_t;

node_t* head = NULL;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
pthread_mutex_t queue_mtx = PTHREAD_MUTEX_INITIALIZER;

void* producer(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 4; ++i) {
        pthread_mutex_lock(&queue_mtx);
        node_t* n = malloc(sizeof(node_t));
        n->value = rand() % 100;
        n->next = head;
        head = n;
        printf("[P%d] Insertó %d\n", id, n->value);
        pthread_cond_signal(&not_empty); // Notifica a un consumidor
        pthread_mutex_unlock(&queue_mtx);
        usleep((rand() % 200000) + 100000); // Retardo aleatorio
    }
    return NULL;
}

void* consumer(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 4; ++i) {
        pthread_mutex_lock(&queue_mtx);
        while (head == NULL) {
            pthread_cond_wait(&not_empty, &queue_mtx);
        }
        node_t* n = head;
        head = head->next;
        printf("[C%d] Consumió %d\n", id, n->value);
        free(n);
        pthread_mutex_unlock(&queue_mtx);
        usleep((rand() % 200000) + 100000);
    }
    return NULL;
}

int main() {
    srand(time(NULL));
    pthread_t prods[3], cons[3];
    int ids[] = {1, 2, 3};

    for (int i = 0; i < 3; ++i) {
        pthread_create(&prods[i], NULL, producer, &ids[i]);
        pthread_create(&cons[i], NULL, consumer, &ids[i]);
    }

    for (int i = 0; i < 3; ++i) {
        pthread_join(prods[i], NULL);
        pthread_join(cons[i], NULL);
    }

    pthread_mutex_destroy(&queue_mtx);
    pthread_cond_destroy(&not_empty);
    return 0;
}

Este código ilustra el flujo típico: los consumidores esperan en bucle mientras la cola esté vacía; los productores notifican tras cada inserción; y el uso combinado de pthread_cond_wait y el mutex garantiza que no ocurran condiciones de carrera ni interbloqueso.

Etiquetas: pthread condvar thread-synchronization producer-consumer C-language

Publicado el 10-3 09:25