Threads en C11: thrd_create, thrd_join y el modelo de concurrencia

La biblioteca <threads.h> es la implementación estándar de la especificación de concurrencia de C11 [C11]. Proporciona un mecanismo para ejecutar múltiples flujos de ejecución (hilos) de forma simultánea, permitiendo el aprovechamiento de procesadores multinúcleo. En esencia, ofrece una capa de abstracción sobre los mecanismos del sistema operativo, como pthreads en entornos POSIX o las APIs de Win32.

Este modelo funciona mediante el uso de identificadores de hilo de tipo thrd_t, que es un tipo opaco (su estructura interna no debe ser consultada directamente), y una función de inicio que debe cumplir estrictamente con la firma int func(void *). La razón de su existencia es permitir que el código sea más portable: en lugar de depender de la semántica de un sistema operativo específico, delegas la responsabilidad al compilador para que mapee estas llamadas a la implementación más eficiente disponible. Deberías usarlo cuando estés desarrollando librerías de bajo nivel que necesiten ser compatibles con múltiples sistemas operativos sin llenar el código de directivas #ifdef para gestionar la creación de hilos. Si te equivocas en la gestión de la sincronización o en el ciclo de vida de los hilos, te enfrentarás a condiciones de carrera (data races) —que resultan en comportamiento indefinido— o a la creación de “hilos zombie” que consumen recursos del sistema sin ser liberados.

#define _POSIX_C_SOURCE 200809L // Para compatibilidad con tipos de tiempo en algunos entornos
#include <threads.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* Estructura para pasar múltiples argumentos a la función del hilo.
   Es crucial que el ciclo de vida de esta estructura sea mayor 
   que el del hilo que la consume. */
typedef struct {
    int id_trabajador;
    int iteraciones;
} worker_config_t;

/* Función que ejecutará el hilo. Debe retornar un int. */
int tarea_pesada(void *arg) {
    worker_config_t *config = (worker_config_t *)arg;
    
    if (config == NULL) {
        return -1;
    }

    // Obtenemos el ID del hilo actual para compararlo
    thrd_t id_actual = thrd_current();
    printf("[Hilo %d] Iniciando tarea...\n", config->id_trabajador);

    for (int i = 0; i < config->iteraciones; i++) {
        // Simulamos una carga de trabajo con un sueño de 500ms
        struct timespec ts = { .tv_sec = 0, .tv_nsec = 500000000L };
        thrd_sleep(&ts, NULL);
        
        printf("[Hilo %d] Progreso: %d/%d\n", 
                config->id_trabajador, i + 1, config->iteraciones);
    }

    printf("[Hilo %d] Completado.\n", config->id_trabajador);
    return config->id_trabajador; // Retornamos el ID como código de salida exitoso
}

int main(void) {
    thrd_t hilo_worker;
    worker_config_t config = { .id_trabajador = 101, .iteraciones = 3 };
    int resultado_final = 0;

    // thrd_create devuelve thrd_success (0) si la operación fue exitosa
    if (thrd_create(&hilo_worker, tarea_pesada, &config) != thrd_success) {
        fprintf(stderr, "Error crítico: No se pudo crear el hilo.\n");
        return EXIT_FAILURE;
    }

    printf("[Main] Hilo principal creado. Esperando al worker...\n");

    /* thrd_join bloquea el hilo actual hasta que 'hilo_worker' termine.
       Es la forma de sincronizar y recuperar el valor de retorno. */
    if (thrd_join(hilo_worker, &resultado_final) == thrd_success) {
        printf("[Main] El worker finalizó con código: %d\n", resultado_final);
    } else {
        fprintf(stderr, "Error al intentar hacer join del hilo.\n");
        return EXIT_FAILURE;
    }

    // Verificación de identidad (aunque en main suele ser el hilo principal)
    if (!thrd_equal(thrd_current(), hilo_worker)) {
        // Esto siempre será cierto tras el join, pues ya no son el mismo contexto
        printf("[Main] Confirmación: El hilo actual es distinto al worker.\n");
    }

    return EXIT_SUCCESS;
}

Análisis del código

En el ejemplo, thrd_create solicita al sistema operativo que asigne recursos para una nueva unidad de ejecución. Fíjate en que pasamos la dirección de config. Esta es una decisión crítica de diseño: el hilo no es una copia de la estructura, sino que opera sobre la misma región de memoria en el heap o en el stack del padre. Si config fuera una variable local de una función que retorna antes de que el hilo termine, entraríamos en comportamiento indefinido.

La función thrd_sleep utiliza struct timespec, que es el estándar para manejar precisión de nanosegundos. A diferencia de usleep (que es una extensión POSIX), thrd_sleep es parte del estándar C11 y está diseñado para integrarse con el modelo de memoria de C11.

Cuando llamamos a thrd_join, el hilo principal entra en un estado de espera. El sistema operativo suspende la ejecución del hilo principal y no la reanuda hasta que tarea_pesada retorna. En este punto, se garantiza una relación de “happens-before”: todas las escrituras de memoria realizadas por el hilo worker son visibles para el hilo principal tras la finalización de thrd_join. El valor retornado por tarea_pesada se captura en resultado_final.

El error frecuente

Uno de los errores más sutiles en sistemas concurrentes es el desprendimiento de la gestión de la memoria del argumento pasado a thrd_create.

// ERROR: Uso de variable local con vida corta
void lanzar_hilo_erroneo() {
    int valor_critico = 42; // Reside en el stack de esta función
    thrd_t h;
    thrd_create(&h, mi_funcion, &valor_critico);
    // La función termina y 'valor_critico' es destruida
} 
// El hilo ejecutará mi_funcion con un puntero a memoria inválida (stack frame ya liberado)

Este error es extremadamente difícil de detectar porque, si el sistema operativo no ha reutilizado ese espacio de memoria del stack inmediatamente, el hilo podría funcionar correctamente en las pruebas de desarrollo, pero fallará aleatoriamente en producción bajo carga. Herramientas como ThreadSanitizer (TSan) son fundamentales para detectar este tipo de violaciones de la integridad de la memoria.

Aunque <threads.h> es el estándar, notarás que en la práctica profesional es común ver pthreads. Esto ocurre porque la implementación de C11 en muchas librerías de sistema es simplemente un wrapper sobre POSIX, y pthreads ofrece funcionalidades avanzadas (como afinidad de CPU o prioridades de tiempo real) que C11 no cubre.

95

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio