Sincronización de hilos con variables condicionales en C

Los mutex proporcionan exclusión mutua, pero a veces se necesita una coordinación más sofisticada entre hilos. Las variables condicionales ofrecen un mecanismo adicional para sincronizar hilos, permitiéndoles esperar hasta que se cumpla una condición específica sin consumir recursos de CPU activamente.

Una variable condicional trabaja en conjunto con un mutex. La condición en sí misma está protegida por el mutex; un hilo debe bloquear el mutex antes de modificar el estado de la condición, y otros hilos no notarán el cambio hasta que también adquieran el mutex.

Inicialización de la variable condicional

Las variables condicionales del tipo pthread_cond_t deben inicializarce antes de su uso. Para variables estáticas, se puede usar la macro PTHREAD_COND_INITIALIZER. Para variables dinámicas, se utiliza la función pthread_cond_init y se debe llamar a pthread_cond_destroy para liberar recursos.

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

Parámetros:

  • cond: Puntero a la variable condicional.
  • attr: Atributos de la condición, generalmente NULL.

Valor de retorno: 0 en éxito, o un código de error.

Destrucción de la variable condicional

int pthread_cond_destroy(pthread_cond_t *cond);

Parámetros:

  • cond: Puntero a la variable condicional a destruir.

Valor de retorno: 0 en éxito, o un código de error.

Señalización de la condición

Estas funciones notifican a los hilos que la condición se ha cumplido. pthread_cond_signal despierta a un hilo que está esperando, mientras que pthread_cond_broadcast despierta a todos los hilos en espera.

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

Valor de retorno: 0 en éxito, o un código de error.

Importante: La señal debe enviarse después de modificar el estado de la condición.

Espera de la condición

pthread_cond_wait bloquea al hilo hasta que la condición se vuelva verdadera. La función recibe un mutex bloqueado; internamente, libera el mutex y coloca al hilo en una lista de espera. Cuando la función retorna, el mutex vuelve a estar bloqueado.

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);

Parámetros:

  • cond: Puntero a la variable condicional.
  • mutex: Puntero al mutex asociado.
  • timeout: Tiempo máximo de espera (para la versión con timeout).

Valor de retorno: 0 en éxito, o un código de error.

Ejemplo de uso

El siguiente código demuestra cómo un hilo espera hasta que una variable compartida alcance el valor 3, momento en el que otro hilo envía una señal para activarlo.

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

pthread_mutex_t mtx;
pthread_cond_t cv;
int shared_counter = 0;

void *espera_condicion(void *arg) {
    int *param = (int *)arg;
    printf("Hilo A: ID %lu, parámetro %d\n", (unsigned long)pthread_self(), *param);
    pthread_mutex_lock(&mtx);
    while (1) {
        pthread_cond_wait(&cv, &mtx);
        if (shared_counter == 3) {
            printf("Hilo A: condición alcanzada, ejecutando acción\n");
            shared_counter = 0;
        }
        printf("Hilo A: valor actual %d\n", shared_counter);
        sleep(1);
    }
    pthread_mutex_unlock(&mtx);
    return NULL;
}

void *incrementa_y_senializa(void *arg) {
    int *param = (int *)arg;
    printf("Hilo B: ID %lu, parámetro %d\n", (unsigned long)pthread_self(), *param);
    while (1) {
        printf("Hilo B: contador antes de incrementar %d\n", shared_counter);
        pthread_mutex_lock(&mtx);
        shared_counter++;
        if (shared_counter == 3) {
            pthread_cond_signal(&cv);
        }
        pthread_mutex_unlock(&mtx);
        sleep(1);
    }
    return NULL;
}

int main() {
    int valor = 100;
    pthread_t hilo_a, hilo_b;
    pthread_mutex_init(&mtx, NULL);
    pthread_cond_init(&cv, NULL);

    if (pthread_create(&hilo_a, NULL, espera_condicion, &valor) == 0) {
        printf("Main: hilo A creado\n");
    }
    if (pthread_create(&hilo_b, NULL, incrementa_y_senializa, &valor) == 0) {
        printf("Main: hilo B creado\n");
    }

    pthread_join(hilo_a, NULL);
    pthread_join(hilo_b, NULL);

    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cv);
    return 0;
}

En este ejemplo, el hilo A espera hasta que shared_counter sea igual a 3. El hilo B incrementa la variable y, al alcanzar el valor 3, envía una señal. Luego, el hilo A resetea la variable a 0.

Las variables condicionales y los mutex también pueden inicializarse estáticamente usando macros: pthread_cond_t cv = PTHREAD_COND_INITIALIZER; y pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;.

Consejo para pruebas

Para ejecutar múltiples instancias de un programa de prueba y guardar los resultados, se puede usar un script auxiliar:

int main(int argc, char **argv) {
    int count = atoi(argv[1]);
    for (int i = 0; i < count; i++) {
        system("./test_program");
    }
    return 0;
}

Ejecutar con ./a.out 10 >> results.txt & para corrrer en segundo plano y almacenar la salida en un archivo.

Etiquetas: pthread condition_variables mutex C POSIX

Publicado el 6-12 17:35