std::thread [C++11] es un objeto que encapsula un hilo de ejecución del sistema operativo. Al instanciar un std::thread, el hilo comienza su ejecución inmediatamente en un flujo separado. Para que el objeto sea válido y el programa no colapse, debemos gestionar su ciclo de vida mediante dos operaciones: join() o detach(). Si un objeto std::thread se destruye mientras todavía es “joinable” (es decir, no se ha llamado a join ni a detach), el runtime de C++ invoca std::terminate(), abortando el programa de forma inmediata.
Esta gestión es necesaria porque el objeto std::thread representa la propiedad de la ejecución, pero no garantiza por sí mismo la sincronización de la terminación. Usamos hilos para ejecutar tareas asíncronas o procesos intensivos sin bloquear el hilo principal, pero debemos ser extremadamente cuidadosos con la vida de los objetos que los hilos tocan. Si pasas un argumento por referencia a un hilo y ese objeto muere antes que el hilo, entrarás en comportamiento indefinido (undefined behavior) debido a una referencia colgante. Debido a esto, los argumentos de std::thread se pasan por valor (se copian) por defecto para asegurar que el hilo tenga sus propios datos, por lo que si necesitas modificar algo en el hilo original, debes usar std::ref para pasar una referencia explícita.
Con la llegada de C++20, se introdujo std::jthread (joining thread) para resolver los problemas de seguridad de std::thread. Un std::jthread aplica el principio RAII (Resource Acquisition Is Initialization) al hilo: si el objeto sale de su ámbito sin haber sido gestionado, invoca automáticamente join() en su destructor, evitando el crash por destrucción de un hilo joinable. Además, integra soporte para cancelación cooperativa mediante std::stop_token, permitiendo que un hilo verifique si debe detenerse de forma segura.
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <functional>
#include <vector>
// Tarea que modifica un dato externo mediante una referencia
void worker(int id, std::string& data) {
// Simulamos trabajo pesado
std::this_thread::sleep_for(std::chrono::milliseconds(150));
data = "Resultado procesado por el hilo " + std::to_string(id);
}
// Tarea cooperativa para std::jthread [C++20]
// El stop_token permite saber si el hilo debe detenerse
void interruptible_task(std::stop_token st) {
while (!st.stop_requested()) {
std::cout << "[jthread] Trabajando...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
std::cout << "[jthread] Tarea interrumpida limpiamente.\n";
}
int main() {
std::string result;
// 1. std::thread tradicional: requiere join() explícito
{
// Usamos std::ref porque worker espera una std::string&
// y std::thread por defecto intentaría copiar el argumento.
std::thread t(worker, 42, std::ref(result));
// Si no llamamos a t.join(), el programa lanzaría std::terminate
// al llegar a la llave de cierre de este scope.
t.join();
std::cout << "Hilo tradicional completado: " << result << "\n";
}
// 2. std::jthread [C++20]: gestión automática y cancelación
{
std::cout << "Iniciando jthread...\n";
std::jthread jt(interruptible_task);
std::this_thread::sleep_for(std::chrono::milliseconds(120));
// Al salir del scope, jt llama automáticamente a
// request_stop() y luego a join(). Es mucho más seguro.
std::cout << "Saliendo del ámbito de jthread...\n";
}
return 0;
}
Para compilar este ejemplo: g++ -std=c++20 -Wall -Wextra -Wpedantic example.cpp -o example
Desglose de la implementación
En el primer bloque de std::thread, la función worker requiere una std::string&. Si hubiéramos pasado simplemente result, el compilador habría intentado realizar una copia del string para el nuevo hilo, lo cual causaría un error de compilación porque worker pide una referencia no constante. std::ref envuelve el argumento para que std::thread pueda pasar la referencia correctamente en su mecanismo interno. La llamada a t.join() es un punto de sincronización: el hilo principal se detiene hasta que worker finaliza.
En el segundo bloque, std::jthread simplifica drásticamente el código. La función interruptible_task acepta un std::stop_token. Este objeto es una abstracción que permite al hilo consultar periódicamente si se ha solicitado su detención (st.stop_requested()). La ventaja fundamental aquí es el comportamiento del destructor: cuando jt sale de su bloque { ... }, su destructor asegura que el hilo termine antes de que el objeto sea destruido, evitando que el hilo intente acceder a recursos locales del main que ya no existen.
El error frecuente
El error más crítico con std::thread es el “destructor crash”. Si creas un hilo que opera sobre variables locales de una función y lanzas el hilo sin join() ni detach(), el programa fallará.
void error_prone_function() {
std::string local_data = "importante";
// ERROR: t no es joinable() cuando sale de la función
std::thread t(worker, 1, std::ref(local_data));
// El programa lanza std::terminate() aquí al destruir 't'
}
Incluso si llamaras a t.detach(), tendrías un problema de lifetime: el hilo seguiría intentando escribir en local_data mientras la función error_prone_function ya ha terminado y local_data ha sido destruida, provocando un segmentation fault o corrupción de memoria. AddressSanitizer (-fsanitize=address) detectaría esto inmediatamente como un use-after-scope.
N° 95