Cómo crear y gestionar hilos en C para Windows con WinAPI

Última actualización: 17/12/2025
Autor: Isaac
  • Los hilos en Windows comparten memoria dentro del mismo proceso, pero cada uno tiene su propia pila y contexto, lo que exige proteger el acceso a datos globales.
  • CreateThread, WaitForSingleObject, mutex y CRITICAL_SECTION son las piezas básicas para crear, esperar y sincronizar hilos en C con la Win32 API.
  • El paso de parámetros a través de LPVOID y el uso de estructuras compartidas permiten que múltiples hilos reutilicen el mismo código trabajando sobre datos distintos.
  • Patrones como contador compartido y productor–consumidor muestran la necesidad de secciones críticas y variables de condición para evitar condiciones de carrera.

sdk, programación

Si programas en C sobre Windows tarde o temprano vas a necesitar ejecutar varias tareas al mismo tiempo: atender eventos de la interfaz, procesar datos en segundo plano, actualizar la consola, etc. Ahí es donde entran en juego los hilos (threads). Aunque en GNU/Linux solemos tirar de pthread y POSIX, en Windows la película cambia: aquí la estrella es la Win32 API y, cuando hablamos de C “puro y duro”, las funciones como CreateThread, WaitForSingleObject, mutex, secciones críticas y variables de condición.

En esta guía vas a ver, con calma pero sin rodeos, cómo crear, gestionar y sincronizar hilos en C para Windows. Partiremos de qué es un hilo, seguiremos con ejemplos básicos de creación, paso de parámetros y devolución de resultados, y terminaremos entrando en materia con secciones críticas, mutex y un clásico de la concurrencia: el patrón productor-consumidor con CRITICAL_SECTION y CONDITION_VARIABLE. Todo ello usando WinAPI y teniendo en cuenta las peculiaridades de Windows frente a otros sistemas.

Qué es un hilo (thread) en Windows y en qué se diferencia de un proceso

Un hilo es, dicho mal y pronto, la unidad mínima de ejecución que el planificador (scheduler) del sistema operativo pone a correr en la CPU. Cuando Windows reparte tiempo de procesador no lo hace entre procesos, lo hace entre hilos. Cada proceso, a su vez, puede tener uno o varios hilos activos.

En Windows, los hilos de un mismo proceso comparten el mismo espacio de direcciones: código ejecutable, datos globales, memoria dinámica (heap) y la mayoría de los recursos abiertos (handles de archivos, sockets, objetos de sincronización…). Cada hilo tiene su propia pila y su propio contexto de registros, pero todos pisan la misma memoria compartida.

Un proceso, en cambio, es una instancia aislada de un programa: tiene su propio espacio de memoria virtual, su tabla de handles, su conjunto de hilos, etc. Crear procesos es bastante más caro que crear hilos, porque el sistema debe montar todo ese entorno aislado. Crear hilos reutiliza gran parte del entorno del proceso.

En máquinas multinúcleo, Windows puede ejecutar varios hilos en paralelo, uno por núcleo lógico. Si tienes un procesador con 4 núcleos, puede haber 4 hilos realmente ejecutándose al mismo tiempo. El resto de hilos listos para ejecutarse van recibiendo pequeños “trozos” de CPU mediante cambios de contexto.

Para qué sirven los hilos en aplicaciones en C sobre Windows

El uso de hilos nos permite, principalmente, dos cosas muy jugosas: por un lado acelerar el tiempo de ejecución repartiendo trabajo entre varios núcleos y, por otro, lograr que nuestra aplicación haga varias cosas a la vez sin bloquearse. Por ejemplo, una aplicación de consola puede seguir respondiendo al teclado mientras otro hilo actualiza la hora en una esquina de la pantalla.

En un contexto de juegos o herramientas para consola de Windows, los hilos son muy útiles para separar lógica de juego, entrada de usuario y tareas de fondo: puedes tener un hilo comprobando continuamente el estado del teclado, otro actualizando animaciones o colores en pantalla y el principal gestionando el bucle principal de juego.

También en programas más “serios” de escritorio o de servidor es habitual usar hilos para procesar peticiones en paralelo, hacer trabajo en segundo plano mientras la interfaz sigue fluida o aprovechar tiempos de espera de I/O para mantener la CPU ocupada con otras tareas.

Modelo de hilo de Windows: HANDLE, función de entrada y recursos asociados

Cuando creas un hilo en Windows mediante la API clásica, el sistema devuelve un HANDLE, es decir, un “mango” u objeto opaco con el que podrás esperar al hilo, consultarlo o cerrarlo; herramientas como Process Hacker permiten inspeccionar esos hilos. Ese handle no es el hilo en sí, sino una referencia al objeto de hilo que mantiene el sistema operativo.

La función que ejecuta el hilo debe cumplir una firma concreta. En la API de Windows se suele usar esta forma estándar:

DWORD WINAPI ThreadFunc(LPVOID arg);

Ese LPVOID arg es un puntero genérico que te permite pasar cualquier dato como parámetro al hilo, desde un entero o una estructura hasta una estructura compleja con varios campos. Dentro del hilo, casteas ese puntero al tipo adecuado.

Cuando el hilo termina, la función devuelve un DWORD con el código de salida del hilo. Desde el hilo principal puedes recuperar ese valor más tarde con la función GetExitCodeThread mientras el handle siga abierto.

Internamente, Windows reserva para cada hilo su propia pila (por defecto suele rondar 1 MB, aunque se puede ajustar en CreateThread) y estructuras internas como el TEB (Thread Environment Block). Además, mantiene toda la información necesaria para los cambios de contexto: registros, punteros de instrucción, estado de la FPU, etc.

  Cómo recuperar videos borrados de CapCut en PC y móvil

Costes de creación y cambio de contexto de hilos en Windows

Crear hilos es más ligero que crear procesos, pero no es gratis. Cada vez que llamas a CreateThread, Windows debe reservar pila, crear el TEB, registrar el hilo en el scheduler y devolver un HANDLE válido. Si necesitas miles de hilos de vida corta, esa sobrecarga se nota y tiene sentido plantearse pools de hilos.

Además está el coste de los cambios de contexto. Cada vez que el scheduler pasa la ejecución de un hilo a otro, tiene que guardar los registros del hilo saliente y restaurar los del entrante, actualizar las pilas, etc. Con muchos hilos listos, el sistema pasa más tiempo cambiando de contexto y moviendo datos en la caché que ejecutando código útil.

Si usas la librería de C estándar de Microsoft, conviene saber que la API de la CRT ofrece funciones como _beginthreadex, que inicializan correctamente la biblioteca de tiempo de ejecución por hilo. El uso directo de CreateThread en hilos que llaman a funciones de la CRT (por ejemplo printf, malloc) puede producir fugas o comportamientos raros si no se tiene cuidado; además, técnicas como la inyección de DLL suelen recurrir a hilos remotos creados por la API.

Crear hilos en C con Windows: CreateThread paso a paso

La función central para lanzar hilos en C con WinAPI es CreateThread. Al llamarla, el hilo empieza a ejecutarse casi de inmediato (salvo que se indique lo contrario con flags). Veamos los parámetros clave que necesitas dominar en el día a día:

  • lpThreadAttributes: estructura de seguridad opcional. En la mayoría de escenarios educativos o de escritorio se pasa NULL y se ignora.
  • dwStackSize: tamaño inicial de pila. Si pasas 0, Windows usa el tamaño por defecto definido en el ejecutable.
  • lpStartAddress: dirección de la función de entrada del hilo, por ejemplo ThreadFunc.
  • lpParameter: puntero a los datos que quieres pasar al hilo. Se recibe como LPVOID en la función.
  • dwCreationFlags: controla cómo se crea el hilo. Con 0, el hilo empieza a ejecutarse inmediatamente. Con otros flags, puedes crear hilos inicialmente suspendidos.
  • lpThreadId: puntero a un DWORD donde Windows escribirá el identificador numérico del hilo. Es útil para depuración o para ciertas operaciones específicas.

La función devuelve un HANDLE al hilo. Si el valor es distinto de NULL, se ha creado correctamente y el código de la función de hilo ya puede estar ejecutándose en paralelo al hilo principal.

Para que el hilo principal espere a que un hilo secundario termine, es muy cómodo utilizar WaitForSingleObject. Le pasas el HANDLE del hilo y un tiempo máximo de espera. Si usas la constante INFINITE, el hilo principal se quedará bloqueado hasta que el secundario finalice:

WaitForSingleObject(threadHandle, INFINITE);

Lo habitual es que el hilo principal lance uno o varios hilos, haga su propio trabajo y, justo antes de terminar el proceso o de usar resultados de esos hilos, espere su finalización con esta función.

Control básico del orden de ejecución: Sleep y comprobaciones

Como el scheduler de Windows es libre de interrumpir y reanudar hilos en casi cualquier momento, a veces conviene introducir esperas artificiales con Sleep solo para fines didácticos. Por ejemplo, si un hilo secundario solo ejecuta un printf que se completa enseguida, puede parecer que no se está respetando el orden de espera porque la salida aparece muy junta.

Añadiendo una llamada a Sleep(ms) dentro del hilo secundario forzamos una pausa visible, lo que permite comprobar de forma clara que el hilo principal realmente espera a que el secundario termine cuando usamos WaitForSingleObject. Esto es muy útil cuando estás aprendiendo y quieres ver el entrelazado de mensajes de varios hilos en consola.

Pasar parámetros a un hilo y devolver resultados

Una de las claves de la programación con hilos es cómo pasarles datos de entrada y cómo recuperar resultados. Con la firma clásica DWORD WINAPI ThreadFunc(LPVOID data) se hace siempre a través del parámetro genérico LPVOID.

Por ejemplo, imagina que quieres lanzar un hilo que sume dos enteros. Desde el hilo principal puedes reservar memoria para un array de int con dos posiciones, copiar ahí los operandos y pasar la dirección del array a CreateThread casteada a LPVOID. Dentro del hilo, haces el casting inverso a int* y ya tienes acceso a los números.

Para devolver un resultado desde el hilo hay varias opciones. Una es usar una variable global protegida si hay más hilos en juego, pero no es especialmente elegante. Otra, mucho más limpia, es usar también memoria dinámica o estructuras compartidas:

  • Reservas un array o estructura con espacio para operandos y resultado.
  • El hilo lee los operandos, calcula la suma y escribe el resultado en el mismo bloque de memoria.
  • El hilo principal, una vez que WaitForSingleObject ha confirmado que el hilo ha finalizado, lee el resultado de esa memoria.

Recuerda siempre que todas las variables globales del proceso son visibles por todos los hilos. Eso facilita el intercambio de información, pero también abre la puerta a montones de condiciones de carrera si no se protege el acceso adecuadamente.

Obtener el ID asignado por Windows a un hilo

Además del HANDLE que devuelve CreateThread, Windows asigna un identificador numérico (un DWORD) a cada hilo. Si te interesa conocer ese ID, solo tienes que pasar la dirección de un DWORD en el sexto parámetro de CreateThread.

Por ejemplo, declaras un DWORD threadId;, y al llamar a CreateThread le pasas &threadId. Cuando la función retorna con éxito, esa variable contiene el ID del hilo. Este dato es útil para depurar, registrar logs o para algunas funciones avanzadas de la API que trabajan con IDs en lugar de con handles.

  Cómo restaurar Windows 11 sin perder tus archivos personales

Varios hilos con la misma función: reutilizando código

Una de las ventajas del modelo de hilos es que la misma función de entrada puede servir para muchos hilos distintos. El truco está en que cada hilo recibe por parámetro un bloque de datos diferente, de manera que todos ejecutan el mismo código pero trabajan sobre datos propios.

Por ejemplo, puedes definir una estructura con la información que necesita cada hilo para mostrar la hora en una posición distinta de la consola: coordenadas X e Y y un flag de salida. En el hilo principal creas dos instancias de esa estructura, con diferentes coordenadas, y lanzas dos hilos pasando la dirección de cada estructura a CreateThread.

Dentro de la función de hilo casteas el LPVOID a un puntero a tu estructura, y a partir de ahí cada hilo usa sus propias coordenadas y su propio flag de salida. El código es exactamente el mismo, pero el comportamiento es distinto para cada hilo gracias a esos datos personalizados.

Así, con una sola función puedes mostrar la hora simultáneamente en varios puntos de la pantalla, cada uno controlado por un hilo diferente, esperando a que cambie el segundo, obteniendo la hora con GetLocalTime y refrescando el texto con sus propias coordenadas.

Sincronización, secciones críticas y condiciones de carrera

Cuando varios hilos acceden a recursos compartidos, es obligatorio hablar de secciones críticas y de sincronización. Una sección crítica es cualquier tramo de código donde un hilo lee o escribe datos que pueden ser usados por otros hilos al mismo tiempo, como una variable global, una lista enlazada, un buffer o incluso el cursor de la consola.

Si permites que dos hilos modifiquen a la vez una estructura compartida, el resultado puede ser completamente impredecible. Por ejemplo, si 300 hilos incrementan un mismo contador global sumando 1, lo lógico sería acabar en 300, pero si no se garantiza exclusión mutua, puedes encontrarte con 298, 295 o cualquier otro valor. Varias sumas se pisan entre sí.

En el caso de la consola de Windows, un problema típico es que el cursor es único. Si un hilo coloca el cursor en unas coordenadas y justo entonces es interrumpido, otro hilo puede cambiar las coordenadas y escribir texto. Cuando el primer hilo retome la ejecución y escriba lo suyo, lo hará donde dejó el segundo, no donde pretendía él. Resultado: texto en posiciones erróneas o mezclado.

Para evitar estos desastres se usan mecanismos de sincronización como mutex, secciones críticas y variables de condición, que permiten serializar el acceso a recursos compartidos y garantizar que solo un hilo está en el tramo crítico al mismo tiempo.

Mutex en Windows: exclusión mutua para secciones críticas sencillas

Un mutex (del inglés “mutual exclusion”) es un objeto de sincronización que solo puede estar en posesión de un hilo a la vez. Mientras un hilo mantiene el mutex bloqueado, cualquier otro hilo que intente adquirirlo tendrá que esperar.

En Windows se crea un mutex con CreateMutex, que devuelve un HANDLE como cualquier otro objeto del sistema. Ese mutex puede tener un nombre global; si lo creas con nombre, otros hilos e incluso otros procesos pueden abrirlo con OpenMutex. La idea básica es:

  1. Crear el mutex una vez al inicio del programa.
  2. Antes de entrar en una sección crítica, esperar al mutex con WaitForSingleObject.
  3. Ejecutar el código que accede al recurso compartido.
  4. Al terminar, liberar el mutex con ReleaseMutex.

Si un hilo llega a la espera del mutex y ya hay otro dentro de la sección crítica, el sistema lo bloquea hasta que el mutex sea liberado. De esta manera se garantiza que el código entre la adquisición y la liberación del mutex se ejecuta de forma efectiva como si fuera atómico respecto a otros hilos.

En aplicaciones de consola es muy habitual encapsular el acceso a pantalla en una función que pida el mutex, trace el texto y lo libere. Por ejemplo, una función TrazarTexto(x, y, texto) podría:

  • Abrir o recibir el HANDLE del mutex compartido.
  • Llamar a WaitForSingleObject para obtenerlo.
  • Colocar el cursor con SetConsoleCursorPosition y escribir con printf.
  • Liberar el mutex con ReleaseMutex y cerrar el HANDLE propio si procede.

Con esto consigues que aunque múltiples hilos escriban en la consola, las secuencias completas de escritura (posición + texto) estén protegidas y no se mezclen. Al terminar el programa, no olvides cerrar el HANDLE del mutex con CloseHandle para liberar recursos.

Ejemplo clásico: 300 hilos incrementando un contador

Un ejemplo muy socorrido para entender las secciones críticas consiste en lanzar cientos de hilos que incrementan un contador global. Sin sincronización, el contador final rara vez coincide con el número de hilos lanzados, porque varias sumas se solapan.

  El solucionador de problemas de Windows no funciona

Si declaras un entero global y creas 300 hilos, cada uno sumando 1 a esa variable y terminando, verás que el resultado puede ser, por ejemplo, 298. Eso demuestra que hay condiciones de carrera: dos hilos leen el valor antiguo, lo incrementan cada uno por su lado y escriben el mismo valor, perdiendo una suma.

Si introduces un mutex alrededor de la operación de incremento, es decir, cada hilo hace WaitForSingleObject, incrementa el contador y luego ReleaseMutex, todos los hilos “forman cola” para hacer su suma. Solo hay un hilo ejecutando ese fragmento en cada momento, y ahora sí, cuando el programa acaba, el contador vale 300.

Este patrón de adquirir un mutex justo antes de tocar el dato compartido y soltarlo en cuanto terminas es la esencia de las secciones críticas bien protegidas y la base de la programación concurrente segura.

Productores y consumidores con CRITICAL_SECTION y ConditionVariable

Cuando necesitas algo más sofisticado que un simple contador, entran en juego los patrones de concurrencia de toda la vida, como el de productor-consumidor. En Windows, además de mutex, tienes mecanismos más ligeros y específicos como CRITICAL_SECTION y CONDITION_VARIABLE, perfectos para coordinar múltiples hilos dentro de un mismo proceso.

Imagina un escenario con 150 hilos productores y 150 hilos consumidores que comparten un buffer de solo 10 elementos. Cada productor genera items para el buffer, durmiendo un tiempo aleatorio entre 0 y 50 ms, y cada consumidor va leyendo items del buffer a su ritmo. No hay garantía sobre el orden de ejecución de los hilos, pero sí necesitamos que el buffer no se llene más de 10 elementos ni se lea cuando está vacío.

Para resolver esto se usan tres piezas básicas:

  • Una CRITICAL_SECTION que protege el acceso al buffer y a sus índices.
  • Una variable de condición para productores, para despertarlos cuando haya espacio libre.
  • Una variable de condición para consumidores, para despertarlos cuando haya elementos disponibles.

El esquema general de los productores es:

  • Entrar en la sección crítica con EnterCriticalSection.
  • Mientras el buffer esté lleno, dormir el hilo con la variable de condición adecuada: se llama a SleepConditionVariableCS asociando la condición a la sección crítica.
  • Cuando hay espacio, producir el item, insertarlo en el buffer y actualizar los índices.
  • Salir de la sección crítica con LeaveCriticalSection y señalar a los consumidores que hay elementos mediante WakeConditionVariable o WakeAllConditionVariable.

Los consumidores siguen una lógica espejo:

  • Entran en la sección crítica.
  • Mientras el buffer esté vacío, esperan en la condición de consumidores.
  • Cuando haya elementos, consumen uno, actualizan el estado del buffer.
  • Abandonan la sección crítica y despiertan a los productores para indicar que se ha liberado espacio.

Con este patrón consigues que ningún hilo productor escriba en el buffer cuando está lleno (se quedará bloqueado hasta que un consumidor libere hueco) y que ningún consumidor intente leer del buffer vacío (se dormirá hasta que haya algún item nuevo). Todo ello, además, con acceso protegido al buffer gracias a la CRITICAL_SECTION.

Hilos y entorno de desarrollo: notas sobre Visual Studio y C#

Aunque aquí estamos centrados en C y la WinAPI, en el ecosistema Windows es muy común encontrarse con ejemplos en Visual C# y .NET usando el espacio de nombres System.Threading. La idea general es la misma: crear hilos, lanzar una función que corre en paralelo, actualizar una barra de progreso o un control gráfico mientras el hilo principal sigue respondiendo.

En C#, por ejemplo, es típico crear una aplicación de Windows Forms con un hilo secundario que manipula una barra de progreso o cambia colores, a la vez que el hilo principal responde a eventos de botones. La lógica es similar: un hilo ejecuta un bucle con Thread.Sleep para no saturar la CPU y va cambiando valores, mientras otro hilo lanza mensajes o atiende clics. En este caso, hay que cuidar también la sincronización de acceso a controles de UI, pero la filosofía de que “un hilo no bloquee al otro” es exactamente la misma.

Lo importante es que, tanto con Thread en C# como con CreateThread o _beginthreadex en C, estás jugando con las mismas reglas de concurrencia, secciones críticas y recursos compartidos. Solo cambia la biblioteca con la que trabajas.

Mirado en conjunto, el modelo de hilos de Windows te permite desde crear un simple hilo que imprime mensajes con Sleep hasta montar arquitecturas complejas con docenas de hilos productores y consumidores coordinados con mutex, secciones críticas y variables de condición; la clave está en entender bien qué comparte cada hilo, cuándo hay que sincronizar y cómo usar sin miedo las funciones de la WinAPI para controlar creación, espera y finalización de cada uno.

Cómo usar herramienta rendimiento en WPR y WPA
Artículo relacionado:
Cómo usar la herramienta de rendimiento en WPR y WPA para analizar Windows a fondo