std::jthread es la evolución necesaria de std::thread [C++20] para resolver dos problemas críticos: la gestión manual de la vida de un hilo y la peligrosidad de interrumpir procesos en ejecución. Mientras que en std::thread estás obligado a llamar explícitamente a join() o detach() antes de que el objeto sea destruido, de lo contrario el programa lanza una excepción que termina en std::terminate(), std::jthread aplica el principio RAII (Resource Acquisition Is Initialization) para la gestión del hilo, realizando automáticamente el join() en su destructor.
El núcleo de esta mejora es la cancelación cooperativa. En sistemas de alta disponibilidad o motores de juegos, nunca debes “matar” un hilo de forma abrupta (como se hacía con APIs nativas de OS), porque el hilo podría estar bloqueando un std::mutex o dejando memoria corrupta. En su lugar, std::jthread utiliza un mecanismo de notificación: el hilo principal le pide al hilo secundario que se detenga, pero es el propio hilo secundario quien decide cuándo y cómo finalizar tras revisar su std::stop_token. Si ignoras este token de parada y entras en un bucle infinito, el destructor de jthread intentará hacer un join() esperando a que el hilo termine, lo que causará que tu aplicación se congele indefinidamente.
#include <iostream>
#include <thread>
#include <chrono>
#include <stop_token>
#include <functional>
// La función acepta std::stop_token como primer parámetro.
// jthread lo inyectará automáticamente si la firma lo permite.
void worker_task(std::stop_token stoken, int id) {
std::cout << "[Worker " << id << "] Iniciado.\n";
// La cancelación es cooperativa: el hilo debe consultar el estado.
while (!stoken.stop_requested()) {
std::cout << "[Worker " << id << "] Procesando datos...\n";
// Simulamos una tarea pesada
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "[Worker " << id << "] Cambio de estado detectado. Limpiando recursos...\n";
}
int main() {
std::cout << "[Main] Iniciando hilo de trabajo...\n";
{
// Creamos un jthread. Automáticamente gestiona su ciclo de vida.
std::jthread jt(worker_task, 1, 42);
// std::stop_callback permite registrar una acción que se ejecutará
// en el hilo que solicite la parada (en este caso, el main).
std::stop_callback<std::function<void()>> callback(jt.get_stop_token(), [] {
std::cout << "[Main] -> Callback de parada ejecutado (limpieza asíncrona).\n";
});
// Simulamos que el hilo principal realiza otras tareas
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "[Main] El scope de jthread va a terminar.\n";
// Al llegar aquí, jt sale de scope.
// El destructor de jthread llama automáticamente a request_stop()
// y luego a join().
}
std::cout << "[Main] Programa finalizado con éxito.\n";
return 0;
}
Para compilar este ejemplo: g++ -std=c++20 -Wall -Wextra -Wpedantic -o example example.cpp
En el código anterior, observa cómo worker_task recibe un std::stop_token. Gracias a la lógica interna de std::jthread, el compilador identifica que el primer argumento es un token de parada y lo pasa automáticamente al hilo, permitiendo que el bucle while sea sensible a las señales de interrupción.
Cuando el objeto jt sale del bloque {}, se dispara su destructor. Este proceso es atómico y seguro: primero llama a request_stop(), lo que cambia el estado interno de la std::stop_source vinculada al hilo, y acto seguido llama a join(). Es por esto que el programa no explota como ocurriría con un std::thread si olvidáramos el join().
La std::stop_callback es una herramienta poderosa para la reactividad. En el ejemplo, el callback se registra utilizando jt.get_stop_token(). Lo fundamental aquí es entender el contexto de ejecución: la función lambda dentro del stop_callback no se ejecuta en el hilo worker, sino en el hilo que invoca request_stop() (en este caso, el hilo principal al destruir jt). Esto es vital para realizar tareas de notificación o limpieza que deben ocurrir de inmediato en el hilo de control.
El error frecuente
El error más sutil con std::jthread no es un error de compilación, sino un bloqueo infinito (deadlock de terminación). Si el hilo worker_task contiene un bucle que no consulta stoken.stop_requested() o si se queda bloqueado en una operación síncrona que no puede ser interrumpida (como una lectura de red bloqueante o un std::this_thread::sleep_for excesivamente largo), ocurrirá lo siguiente:
// Código con error lógico
std::jthread jt([](std::stop_token stoken) {
while (true) { // ERROR: No consulta el token
// Si el hilo se queda atrapado aquí...
std::this_thread::sleep_for(std::chrono::hours(1));
}
});
// Al salir del scope, jt llama a request_stop() y luego a join().
// Como el hilo nunca sale del bucle, join() nunca retorna.
// El programa se queda colgado para siempre.
Este error es difícil de detectar en entornos de testing rápidos, pero en producción causará que el proceso nunca termine de forma limpia. AddressSanitizer no detectará esto como un error de memoria, ya que es un problema de lógica de sincronización. Para evitarlo, asegúrate siempre de que cada bucle de trabajo en un hilo tenga una condición de salida basada en el stop_token.
N° 96