std::thread, std::jthread y Gestion de Hilos
Aprende a crear y gestionar hilos con std::thread y std::jthread de C++20. Comprende join vs detach, cancelacion cooperativa con stop tokens, almacenamiento local de hilo y manejo de excepciones a traves de limites de hilo.
Por Que Concurrencia?
Las CPUs modernas tienen multiples nucleos, y para aprovecharlos tu programa debe ejecutar trabajo concurrentemente. C++11 introdujo una biblioteca de hilos portable en , y C++20 la mejoro con std::jthread. Un hilo es un camino de ejecucion independiente que comparte el mismo espacio de direcciones que el hilo que lo creo — lo cual es tanto su poder como su peligro.
A diferencia de los procesos, los hilos comparten memoria. Eso hace que la comunicacion sea rapida pero tambien introduce carreras de datos si dos hilos leen y escriben la misma variable sin sincronizacion. Antes de abordar la sincronizacion (proxima leccion), primero aprendamos como crear, gestionar y detener hilos de forma limpia.
Creando Hilos con std::thread
Creas un std::thread pasandole un llamable (funcion, lambda, functor) y cualquier argumento. El hilo comienza a ejecutarse inmediatamente al ser construido.
#include <iostream>
#include <thread>
void greet(const std::string& name, int times) {
for (int i = 0; i < times; ++i) {
std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
}
}
int main() {
// Launch a thread running greet("Alice", 3)
std::thread t1(greet, "Alice", 3);
// Lambda-based thread
std::thread t2([]() {
std::cout << "Running in a lambda thread\n";
});
// MUST join or detach before t1/t2 are destroyed
t1.join(); // block until t1 finishes
t2.join();
std::cout << "All threads finished\n";
return 0;
}join vs detach
Cada std::thread debe tener una accion de join o detach antes de que se llame a su destructor. Si un hilo "joinable" es destruido, se llama a std::terminate() — el programa se aborta inmediatamente.
join() — El hilo que llama se bloquea hasta que el hilo objetivo finaliza. El hilo que llama espera a que se complete.
detach() — El hilo se ejecuta independientemente en segundo plano. No puedes esperar por el ni verificar su estado. Los hilos separados son peligrosos porque continuan ejecutandose despues de que la funcion main retorne, potencialmente accediendo a objetos destruidos.
Regla general: siempre haz join a los hilos a menos que tengas una razon especifica para separarlos. Los hilos separados son una fuente comun de errores de cierre.
std::jthread — El Hilo Mejorado (C++20)
std::jthread (joining thread) resuelve los dos mayores problemas con std::thread: automaticamente hace join en su destructor (sin terminacion si olvidas join) y soporta cancelacion cooperativa a traves de std::stop_token.
Excepciones y Almacenamiento Local de Hilo
Excepciones: Las excepciones no se propagan a traves de limites de hilo. Si un hilo lanza y la excepcion no es capturada dentro de ese hilo, se llama a std::terminate(). Para transferir excepciones entre hilos, usa std::promise/std::future o std::exception_ptr (cubierto en la leccion de Futures).
Almacenamiento local de hilo (thread_local): Las variables declaradas con thread_local tienen copias separadas para cada hilo. Se construyen cuando el hilo accede por primera vez a la variable y se destruyen cuando el hilo sale:
thread_local int request_id = 0; // cada hilo tiene su propio request_id
void handle_request(int id) {
request_id = id;
// ... todo el codigo en este hilo ve este request_id
}Usos comunes: caches por hilo, IDs de solicitud para registro, reutilizacion de buffers. Ten cuidado con el costo de thread_local: cada acceso involucra una busqueda indirecta en la mayoria de las plataformas.
Los errores mas peligrosos de hilos son aquellos que parecen funcionar en pruebas pero fallan en produccion bajo carga.
Prefiere std::jthread sobre std::thread — el join automatico y los stop tokens eliminan errores comunes. Nunca separes hilos a menos que esten gestionados por un pool de hilos. Siempre pasa datos compartidos por valor o a traves de mecanismos de sincronizacion, no por referencia cruda. Usa std::ref() intencionalmente, no por defecto. Para trabajo pesado de CPU, crea un hilo por nucleo (usa std::thread::hardware_concurrency() para consultar). Para trabajo con E/S, considera coroutines o E/S asincrona en su lugar.
An exception thrown inside a thread that is not caught within that thread calls std::terminate(). Exceptions do not propagate automatically to the joining thread.
To transfer exceptions between threads, capture them with std::current_exception() inside the worker thread and store them in a std::exception_ptr. The joining thread can then rethrow with std::rethrow_exception(). The std::future mechanism (covered in Lesson 4) handles this automatically.
std::threadstarts executing immediately; you mustjoin()ordetach()before its destructor runsstd::jthread(C++20) auto-joins in its destructor and supports cooperative cancellation viastd::stop_tokenthread_localgives each thread its own independent copy of a variable — no synchronization neededstd::thread::hardware_concurrency()tells you how many threads the hardware can run concurrently- Exceptions in a thread do not propagate to the caller — use
std::exception_ptrorstd::futureto transfer them - Never let a detached thread reference local variables from the spawning scope
Quiz — Pon a prueba tus conocimientos
(15 XP)1. Que sucede si un `std::thread` joinable es destruido sin que se llame a join o detach?
2. Como soporta `std::jthread` la cancelacion cooperativa?
3. Que hace `std::ref()` al pasar argumentos a una funcion de hilo?