3 - Creación y Gestión de Hilos con la API de Windows

En este tema vemos cómo crear, finalizar y sincronizar hilos en Windows usando C. Compararemos CreateThread y _beginthreadex, repasaremos la espera con WaitForSingleObject y WaitForMultipleObjects, y aplicaremos todo en un ejemplo que calcula factoriales en paralelo.

3.1 Creación de hilos con CreateThread() y/o _beginthreadex()

CreateThread es la API de Windows para lanzar hilos, pero si el código usa la CRT (malloc, printf, etc.) es preferible _beginthreadex, que inicializa estructuras internas de la runtime y evita fugas. Ambos retornan un HANDLE al hilo y un ID opcional.

3.2 Finalización de hilos: ExitThread() y retorno normal

La forma normal de terminar un hilo es retornar desde la función de entrada. ExitThread fuerza la salida inmediata; solo debe usarse en casos especiales porque omite limpieza automática de algunas librerías.

3.3 Espera por hilos: WaitForSingleObject() y WaitForMultipleObjects()

WaitForSingleObject bloquea hasta que un HANDLE señalado (como un hilo) termina. WaitForMultipleObjects permite esperar varios handles a la vez pero tiene límite de 64. Si se crean más hilos, se puede esperar en grupos o iterar con la versión simple.

3.4 Hilos desacoplados (sin join) y cierre de handles con CloseHandle()

Si no necesitas sincronizar con un hilo, puedes dejarlo correr en background, pero siempre cierra el HANDLE con CloseHandle para no filtrar recursos. No es recomendable terminar la aplicación mientras hilos en background manipulan memoria compartida.

3.5 Paso de argumentos a un hilo mediante estructuras

La función de entrada recibe un LPVOID. Se suele pasar la dirección de una estructura con todos los datos requeridos (p. ej., índice, punteros a buffers compartidos, parámetros). Así se evitan globales y se facilita la extensión del código.

3.6 Retorno de resultados: estructuras compartidas, punteros y sincronización básica

El hilo puede escribir resultados en una zona compartida (vector de resultados) si cada hilo tiene un índice exclusivo, evitando colisiones. Cuando se necesita escribir en la misma estructura, hay que usar sincronización (critical sections, mutex). GetExitCodeThread permite leer el código de salida del hilo, pero para datos complejos es mejor usar memoria compartida.

3.7 Aplicación en C (CLion, Windows): Cálculo concurrente de factoriales

El programa siguiente pide una lista de números, crea un hilo por cada uno, calcula su factorial y guarda el resultado en un arreglo compartido. El hilo principal espera a todos y luego imprime los resultados. Se limita la cantidad a 32 para no superar el límite de WaitForMultipleObjects.

#include <windows.h>
#include <process.h>   /* _beginthreadex */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_HILOS 32

typedef struct {
  int numero;
  size_t indice;
  unsigned long long *resultados;
} Task;

unsigned __stdcall calcular_factorial(void *arg) {
  Task *t = (Task *)arg;
  unsigned long long acc = 1;
  for (int i = 2; i <= t->numero; i++) {
    acc *= (unsigned long long)i;
  }
  t->resultados[t->indice] = acc;
  return 0;
}

int parsear_numeros(int *dest, size_t max_count) {
  char linea[512];
  printf("Ingrese numeros separados por espacio (max %zu): ", max_count);
  if (!fgets(linea, sizeof linea, stdin)) {
    return -1;
  }
  size_t count = 0;
  char *token = strtok(linea, " \t\r\n");
  while (token && count < max_count) {
    dest[count++] = atoi(token);
    token = strtok(NULL, " \t\r\n");
  }
  return (int)count;
}

int main(void) {
  int numeros[MAX_HILOS];
  int cantidad = parsear_numeros(numeros, MAX_HILOS);
  if (cantidad <= 0) {
    fputs("No se ingresaron numeros validos.\n", stderr);
    return EXIT_FAILURE;
  }

  HANDLE handles[MAX_HILOS] = {0};
  Task tareas[MAX_HILOS];
  unsigned long long resultados[MAX_HILOS] = {0};

  for (int i = 0; i < cantidad; i++) {
    tareas[i].numero = numeros[i];
    tareas[i].indice = (size_t)i;
    tareas[i].resultados = resultados;

    uintptr_t h = _beginthreadex(
      NULL,            /* seguridad por defecto */
      0,               /* stack por defecto */
      calcular_factorial,
      &tareas[i],      /* argumento */
      0,               /* flags */
      NULL             /* thread id opcional */
    );
    if (h == 0) {
      fprintf(stderr, "No se pudo crear el hilo para el numero %d\n", numeros[i]);
      cantidad = i; /* solo esperar los creados */
      break;
    }
    handles[i] = (HANDLE)h;
  }

  DWORD espera = WaitForMultipleObjects((DWORD)cantidad, handles, TRUE, INFINITE);
  if (espera == WAIT_FAILED) {
    fprintf(stderr, "WaitForMultipleObjects fallo (%lu)\n", GetLastError());
  }

  for (int i = 0; i < cantidad; i++) {
    if (handles[i]) {
      CloseHandle(handles[i]);
    }
  }

  printf("Resultados:\n");
  for (int i = 0; i < cantidad; i++) {
    printf("%d! = %llu\n", numeros[i], resultados[i]);
  }

  return EXIT_SUCCESS;
}

El cálculo de factorial usa unsigned long long; para valores grandes puede desbordar (overflow). Puedes cambiar la carga de trabajo o reemplazar el cálculo por algo más costoso para observar mejor el paralelismo. Si necesitas más de 64 hilos, espera en grupos o usa WaitForSingleObject en un bucle.

Salida de factoriales concurrentes en Windows con tiempos y resultados