07/03/2016
En el desarrollo de software moderno, especialmente en el mundo de los videojuegos y aplicaciones de alto rendimiento, la capacidad de realizar múltiples tareas simultáneamente no es un lujo, sino una necesidad. Aquí es donde entra en juego la concurrencia, el arte de ejecutar diferentes secuencias de operaciones al mismo tiempo. Desde la introducción del estándar C++11, los desarrolladores contamos con una herramienta poderosa y estandarizada para manejar esto: la librería <thread> y, en particular, la clase std::thread. Esta guía te llevará a través de los conceptos fundamentales para que puedas empezar a aprovechar el verdadero potencial de los procesadores multinúcleo en tus proyectos.

- ¿Qué es un Hilo de Ejecución?
- La llegada de `std::thread`: Un Estándar para la Concurrencia
- Creando y Lanzando tu Primer Hilo
- El Ciclo de Vida de un Hilo: `join()` vs. `detach()`
- Pasando Argumentos a los Hilos
- Funciones Miembro Esenciales de `std::thread`
- El Gran Desafío: La Sincronización
- Preguntas Frecuentes (FAQ)
¿Qué es un Hilo de Ejecución?
Imagina que tu programa es una gran fábrica. El hilo principal (el que ejecuta tu función main()) es el gerente que sigue la lista principal de tareas. Un hilo de ejecución, o simplemente "hilo", es como contratar a un trabajador adicional al que le asignas una tarea específica. Este trabajador puede realizar su labor de forma independiente y al mismo tiempo que el gerente y otros trabajadores. De esta manera, la fábrica puede producir mucho más en menos tiempo.
En términos técnicos, un hilo es la secuencia más pequeña de instrucciones que puede ser gestionada de forma independiente por el planificador del sistema operativo. Un solo proceso (tu aplicación) puede tener múltiples hilos ejecutándose en paralelo, cada uno con su propio contador de programa, pila de llamadas y estado de ejecución, pero compartiendo el mismo espacio de memoria. Esto último es tanto una gran ventaja (facilita la comunicación entre hilos) como un gran desafío (requiere una gestión cuidadosa para evitar conflictos).
La llegada de `std::thread`: Un Estándar para la Concurrencia
Antes de C++11, crear y gestionar hilos requería el uso de librerías específicas de la plataforma, como pthreads en sistemas POSIX (Linux, macOS) o la API de Windows. Esto significaba que el código no era portable y requería adaptaciones para cada sistema operativo. Con la llegada de std::thread, C++ nos proporciona una interfaz unificada y portable para trabajar con hilos, simplificando enormemente el desarrollo de software concurrente.
La clase std::thread, definida en el encabezado <thread>, representa un único hilo de ejecución. Al crear un objeto de esta clase, se inicia inmediatamente un nuevo hilo que comienza a ejecutar la función que le hemos proporcionado.
Creando y Lanzando tu Primer Hilo
La creación de un hilo es sorprendentemente sencilla. Solo necesitas crear una instancia de std::thread y pasarle como argumento en el constructor la función que deseas que se ejecute en ese nuevo hilo.

Veamos un ejemplo clásico para ilustrarlo:
#include <iostream> #include <thread> // Una función simple que se ejecutará en un hilo void tarea_uno() { // Simula hacer algún trabajo std::cout << "El hilo de la tarea uno está trabajando.\n"; } // Otra función que recibe un argumento void tarea_dos(int valor) { std::cout << "El hilo de la tarea dos recibió el valor: " << valor << "\n"; } int main() { // Creamos un hilo que ejecutará la función tarea_uno() std::thread primer_hilo(tarea_uno); // Creamos un segundo hilo que ejecutará tarea_dos(100) std::thread segundo_hilo(tarea_dos, 100); std::cout << "El hilo main, primer_hilo y segundo_hilo ahora se ejecutan de forma concurrente...\n"; // Sincronizamos los hilos (esperamos a que terminen) primer_hilo.join(); segundo_hilo.join(); std::cout << "Todas las tareas han finalizado.\n"; return 0; }En este código, main crea dos hilos, primer_hilo y segundo_hilo. Tan pronto como estas líneas se ejecutan, las funciones tarea_uno y tarea_dos comienzan a correr en paralelo con la función main. El orden exacto en que se imprimen los mensajes puede variar en cada ejecución, lo que demuestra la naturaleza no determinista de la planificación de hilos.
El Ciclo de Vida de un Hilo: `join()` vs. `detach()`
Una vez que un hilo ha sido lanzado, el hilo padre (el que lo creó) debe decidir qué hacer con él. No puedes simplemente olvidarte de un hilo activo. Existen dos opciones principales, y la elección es crucial:
1. Unirse con `join()`
Llamar a la función join() en un objeto std::thread es la forma más común y segura de manejar el fin de un hilo. Esta llamada hace que el hilo que la invoca (en nuestro ejemplo, el hilo main) se bloquee y espere hasta que el hilo en cuestión (primer_hilo) haya completado su ejecución. Es como un gerente que espera a que un empleado termine su tarea antes de cerrar la fábrica por la noche. Usar join garantiza que todos los recursos utilizados por el hilo se liberen correctamente y que cualquier resultado de su trabajo esté disponible antes de continuar.
2. Separarse con `detach()`
La alternativa es llamar a detach(). Esta acción desvincula el hilo de ejecución del objeto std::thread. El hilo se convierte en un "hilo demonio" que continuará ejecutándose en segundo plano de forma independiente. El objeto std::thread original ya no representa a ese hilo. Esto es útil para tareas de "disparar y olvidar", como un registro de eventos en segundo plano. Sin embargo, es una opción peligrosa. Si el programa principal termina, todos los hilos separados se terminan abruptamente, sin importar lo que estuvieran haciendo. Además, si un hilo separado intenta acceder a datos que fueron destruidos cuando el hilo principal finalizó, el resultado es un comportamiento indefinido (¡un crash casi seguro!). Se debe usar detach con extremo cuidado.
Importante: Un objeto std::thread que representa un hilo de ejecución activo (es decir, que es `joinable()`) debe ser unido (`join`) o separado (`detach`) antes de que sea destruido (por ejemplo, al salir de su ámbito). Si no lo haces, el destructor de std::thread llamará a std::terminate, lo que abortará tu programa de forma inmediata.
Pasando Argumentos a los Hilos
Como vimos en el ejemplo, puedes pasar argumentos a la función de tu hilo simplemente añadiéndolos al constructor de std::thread después del nombre de la función. Sin embargo, hay un detalle fundamental: por defecto, los argumentos son copiados al almacenamiento interno del nuevo hilo. Esto significa que si pasas una variable, el hilo trabajará con una copia, no con la original.
Si necesitas que el hilo modifique la variable original, debes pasarla por referencia. Para ello, C++ proporciona la función de ayuda std::ref() del encabezado <functional>.

Ejemplo:
void modificar_valor(int& valor) { valor = 99; } int mi_numero = 10; // El hilo trabajará sobre una copia, mi_numero seguirá siendo 10 // std::thread t1(modificar_valor, mi_numero); // Usando std::ref, el hilo modificará la variable original std::thread t2(modificar_valor, std::ref(mi_numero)); t2.join(); // Ahora, mi_numero es 99Funciones Miembro Esenciales de `std::thread`
La clase std::thread ofrece varias funciones útiles para su gestión. Aquí tienes una tabla con las más importantes:
| Función | Descripción |
|---|---|
joinable() | Devuelve true si el objeto thread representa un hilo de ejecución activo que puede ser unido o separado. |
get_id() | Devuelve un identificador único para el hilo. Es útil para depuración y para diferenciar hilos. |
native_handle() | Devuelve el manejador de hilo subyacente específico de la plataforma (por ejemplo, un pthread_t). Para uso avanzado. |
hardware_concurrency() [Estática] | Devuelve el número de hilos de hardware que pueden ejecutarse concurrentemente. Es una pista, no un valor exacto, pero muy útil para dimensionar un pool de hilos. |
swap() | Intercambia el estado de dos objetos std::thread. |
El Gran Desafío: La Sincronización
El poder de los hilos viene con una gran responsabilidad: la sincronización. Cuando múltiples hilos acceden y modifican datos compartidos al mismo tiempo, pueden ocurrir las temidas "condiciones de carrera" (race conditions), donde el resultado final depende del orden impredecible en que los hilos son ejecutados por el sistema operativo. Esto conduce a bugs erráticos y muy difíciles de depurar.
Para evitar esto, C++ proporciona herramientas de sincronización como std::mutex (para crear secciones críticas donde solo un hilo puede entrar a la vez) y std::atomic (para operaciones simples sobre tipos de datos que se garantizan indivisibles). Aunque explorar estas herramientas en profundidad requiere su propio artículo, es vital ser consciente de que cualquier acceso a memoria compartida debe ser protegido.
Preguntas Frecuentes (FAQ)
¿Qué pasa si una función de un hilo lanza una excepción?
Si una excepción no es capturada dentro de la función del hilo, el programa llamará a std::terminate, finalizando la aplicación de forma abrupta. Por lo tanto, es una buena práctica envolver el contenido de la función de un hilo en un bloque try...catch para manejar cualquier posible excepción de forma controlada.
¿Cuántos hilos debería crear?
No hay una respuesta única. Crear demasiados hilos puede ser contraproducente. Cada hilo consume recursos y el cambio de contexto entre ellos (el trabajo que hace el sistema operativo para pausar un hilo y reanudar otro) tiene un costo. Una buena regla general es empezar con un número de hilos cercano al valor devuelto por std::thread::hardware_concurrency(), que suele corresponder al número de núcleos lógicos de la CPU, y ajustar a partir de ahí según las mediciones de rendimiento.
¿Cómo puede un hilo devolver un valor a quien lo creó?
La función de un hilo no tiene un mecanismo de retorno directo como una llamada a función normal. Para comunicar resultados, se pueden usar varias estrategias: pasar una referencia a una variable que el hilo modificará (protegida con un mutex), o utilizar mecanismos de concurrencia más avanzados de C++ como std::promise y std::future, diseñados específicamente para este propósito.
Si quieres conocer otros artículos parecidos a Hilos en C++: Guía para la Concurrencia puedes visitar la categoría Juegos.
