2 - Introducción a los Hilos en C (Windows Threads)

En Windows los hilos permiten ejecutar varias rutinas dentro del mismo proceso, compartiendo memoria pero con pilas y contextos independientes. En este tema repasamos qué es un hilo, cómo se lanza con _beginthreadex (o CreateThread) y cómo sincronizar su finalización.

2.1 ¿Qué es un hilo en Windows y cómo se diferencia de un proceso?

Un hilo es la unidad básica de ejecución planificada por el scheduler (planificador del sistema operativo). Todos los hilos de un proceso comparten el mismo espacio de direcciones (código, datos, heap) y los mismos descriptores de archivo/handles. Cada proceso puede tener uno o más hilos; en cambio, cada proceso tiene su propio espacio de memoria y recursos aislados.

2.2 Hilos de Windows: HANDLE, función de entrada (LPTHREAD_START_ROUTINE)

Para crear un hilo en Windows se define una función de entrada con la firma DWORD WINAPI ThreadFunc(LPVOID arg) (o unsigned __stdcall con _beginthreadex). Al lanzar el hilo se recibe un HANDLE que permite esperarlo o forzar su terminación (no recomendado). Cuando el hilo termina, retorna un valor que puede recuperarse con GetExitCodeThread.

2.3 Cómo se ejecutan los hilos dentro de un mismo proceso en Windows

Todos los hilos comparten el código y los datos del proceso, por lo que pueden leer y escribir las mismas variables. Esto facilita el intercambio de información pero exige protección frente a condiciones de carrera si se escribe en estructuras compartidas. El scheduler asigna rebanadas de CPU a cada hilo según su prioridad y estado (listo, bloqueado, ejecutando).

2.4 Stack, contexto de hilo y recursos asociados

Cada hilo tiene su propia pila y contexto de registros. El tamaño de pila por defecto suele ser 1 MB, configurable al crear el hilo. Además se crean estructuras internas (TEB, Thread Environment Block) y un handle para administrarlo. Como comparten heap y archivos, es importante coordinar el acceso para evitar corrupción.

2.5 Costos de creación y cambio de contexto en Windows

  • Creación: lanzar un hilo implica reservar pila, TEB y registrar el hilo en el scheduler. Es más liviano que crear un proceso, pero sigue teniendo costo; conviene reutilizar hilos si se crean miles.
  • Cambio de contexto: al alternar entre hilos, Windows guarda/restaura registros y pilas. Si hay muchos hilos listos, se incrementan los cambios de contexto y el overhead de cache.
  • API: _beginthreadex inicializa la CRT (C Runtime) correctamente; CreateThread puede causar fugas si el hilo usa funciones de la CRT (malloc, printf, etc.).

2.6 Aplicación en C (CLion, Windows): Hola desde múltiples hilos

El programa siguiente crea N hilos, cada uno imprime su identificador y duerme un instante. El hilo principal espera a todos y compara el tiempo frente a una versión secuencial. Usa _beginthreadex para ser seguro con la CRT.

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

typedef struct {
  int id;
  int work_ms;
} ThreadArgs;

unsigned __stdcall thread_func(void *arg) {
  ThreadArgs *info = (ThreadArgs *)arg;
  printf("Hola, soy el hilo %d (tid=%lu)\n", info->id, GetCurrentThreadId());
  Sleep(info->work_ms); /* simulamos trabajo */
  return 0;
}

int main(void) {
  int n = 0;
  printf("Cantidad de hilos a crear: ");
  if (scanf("%d", &n) != 1 || n <= 0) {
    fputs("Valor invalido.\n", stderr);
    return EXIT_FAILURE;
  }

  HANDLE *handles = calloc((size_t)n, sizeof(HANDLE));
  ThreadArgs *args = calloc((size_t)n, sizeof(ThreadArgs));
  if (!handles || !args) {
    fputs("No hay memoria suficiente.\n", stderr);
    return EXIT_FAILURE;
  }

  DWORD inicio_hilos = GetTickCount();
  for (int i = 0; i < n; i++) {
    args[i].id = i;
    args[i].work_ms = 50; /* ajuste aqui la carga de trabajo */
    uintptr_t h = _beginthreadex(
      NULL,            /* seguridad por defecto */
      0,               /* stack por defecto */
      thread_func,     /* funcion de entrada */
      &args[i],        /* argumento */
      0,               /* flags */
      NULL             /* thread id opcional */
    );
    if (h == 0) {
      fprintf(stderr, "No se pudo crear el hilo %d\n", i);
      n = i; /* solo esperar los que ya existen */
      break;
    }
    handles[i] = (HANDLE)h;
  }

  for (int i = 0; i < n; i++) {
    if (handles[i]) {
      WaitForSingleObject(handles[i], INFINITE);
      CloseHandle(handles[i]);
    }
  }
  DWORD fin_hilos = GetTickCount();

  DWORD inicio_seq = GetTickCount();
  for (int i = 0; i < n; i++) {
    printf("[Secuencial] Hola, soy el hilo simulado %d\n", i);
    Sleep(50);
  }
  DWORD fin_seq = GetTickCount();

  printf("Tiempo con hilos: %lu ms\n", (unsigned long)(fin_hilos - inicio_hilos));
  printf("Tiempo secuencial: %lu ms\n", (unsigned long)(fin_seq - inicio_seq));

  free(handles);
  free(args);
  return EXIT_SUCCESS;
}

El ejemplo usa _beginthreadex en lugar de CreateThread porque inicializa correctamente la CRT para cada hilo. También usa WaitForSingleObject en bucle, evitando el límite de 64 handles de WaitForMultipleObjects. Ajusta work_ms para simular más o menos trabajo y observar la diferencia en tiempos.

Salida de ejemplo con varios hilos creados en Windows y sus tiempos de ejecución