Bienvenidos sean a este post, hoy veremos otra forma de manejar los threads.
En el post anterior vimos como utilizar un thread para correr una funcion. Pero a partir de C++20 se agrega un nuevo tipo de thread llamada joined thread, jthread. Como provee una interfaz similar a thread, podemos reemplazar a estos con jthread. Porque este envuelve a thread y termina delegando todo a thread. Vamos a tomar uno de los codigos del post anterior:
#include <iostream>
#include <thread>
class Test
{
public:
Test() = default;
void operator()() {
++estado;
std::cout << estado << std::endl;
}
private:
int estado = 0;
};
int main()
{
std::thread hilo{Test()};
hilo.join();
return 0;
}
En este codigo tenemos una clase donde aplicamos una sobrecarga del operador ()) y en esta incrementaba al valor de la propiedad privada y nos lo mostraba en la consola. La particularidad estaba en el main porque pasabamos a la clase con el operador de funcion al thread y para que funcione le aplicabamos un join al mismo. Tomemos este codigo y modifiquemos el main de la siguiente manera:
int main()
{
std::jthread hilo{Test()};
return 0;
}
Siguiendo con el mismo archivo de encabezado, solamente modificamos al objeto donde en lugar de usar a thread ahora es jthread. Y tambien eliminamos el llamado a join porque ahora esta de manera implicita con este objeto. Pero recuerden que para poder compilarlo debe ser de la siguiente manera:
$ g++ --std=c++20 threads.cpp -o threads
En este caso deben agregar la opcion para especificar el estandar porque de lo contrario no lo encontrara. Con nuestro codigo compilado, debemos tener la siguiente salida:
$ ./threads
1
$
Si lo prueban con el codigo original veran que es la misma salida pero si el compilador no soporta este estandar? Se puede hacer lo mismo mediante RAII, del cual hablamos en este post, porque este se puede adaptar perfectamente a threads. Analicemos el siguiente ejemplo:
#include <iostream>
#include <thread>
class raii_thread
{
public:
explicit raii_thread(std::thread& h): hilo(std::move(h)) {}
~raii_thread() { hilo.join(); }
private:
std::thread hilo;
};
void test()
{
std::cout << "Testeando el join de thread" << std::endl;
}
int main()
{
std::thread h{test};
raii_thread r{h};
return 0;
}
Primero definimos una clase para aplicar al RAII. Tenemos una propiedad privada que sera de tipo thread. En la parte publica definimos un constructor que iniciara al objeto privado con el recibido y luego tenemos un destructor que aplicara al metodo join para indicarle al thread principal que este finalizo. Luego tenemos una funcion que mostrara un mensaje para indicar que estamos testeando el thread. En el main, definimos un objeto de tipo thread y le asignamos la funcion test. Para luego crear un objeto del tipo de la clase. Compilemos y veamos como es la salida:
$ ./threads
Testeando el join de thread
$
Pero por que funciono? Simplemente porque en el constructor le pasamos el thread y cuando se deja de usar, se llama al destructor y este invoca a join para informarle al maint thread que puede continuar. Por esta razon funciono pefectamente. Sin embargo, este codigo presenta un inconveniente. No posee una forma de comprobar si el objeto no fue desafectado antes. Tomemos la clase del codigo anterior y hagamos el siguiente cambio:
class raii_thread
{
public:
explicit raii_thread(std::thread& h): hilo(std::move(h)) {}
~raii_thread() { if (hilo.joinable()) hilo.join(); }
private:
std::thread hilo;
};
La unica modificacion es en el destructor donde agregamos un condicional. Este verifica si el thread sigue activo mediante joinable. En caso de ser verdadero, aplica el join al objeto de lo contrario no hara nada, porque como dijimos antes esto indica que el thread ya fue deafectado. Volvamos a jthread, para ello analicemos el siguiente codigo:
#include <chrono>
#include <iostream>
#include <thread>
#include <vector>
using namespace::std::literals;
auto func = [](std::stop_token stoken) {
int contar{0};
auto thread_id = std::this_thread::get_id();
std::stop_callback callBack(stoken, [&contar, thread_id] {
std::cout << "Thread id: " << thread_id
<< "; contar: " << contar << '\n';
});
while (contar < 10) {
std::this_thread::sleep_for(0.2s);
++contar;
}
};
int main() {
std::cout << '\n';
std::vector<std::jthread> vecThreads(10);
for(auto& thr: vecThreads) thr = std::jthread(func);
std::this_thread::sleep_for(1s);
for(auto& thr: vecThreads) thr.request_stop();
std::cout << '\n';
return 0;
}
Este ejemplo es para aplicar una funcion de los jthread como es request_stop. Lo primero que haremos sera definir una funcion lambda o anonimma llamada func. Primer detalle, este recibe un objeto de tipo stop_token y este se utiliza para indicar si se puede detener al thread. Iniciamos una variable para contar y la otra sera usada para almacenar el id de cada thread creado. El siguiente objeto sera de tipo stop_callback, la cual provee un objeto de tipo RAII que registra una funcion callback para un objeto asociado del tipo anterior (stop_token). Este recibe tres argumentos: el token de func, y despues pasamoss otra funcion lambda y como argumentos recibiremos el contador y el id del thread para mostrarlos en consola. Para luego tener un bucle que se ejecutara mientras contar sea menor a 10. En este lo detendremos por 0..2 segundos y luego incrementamos el valor de contar. En el main, creamos un objeto de tipo vector que contendra objetos de tipo jthread y tendra 10 elementos. El siguiente bucle le asigna a cada elemento un thread con la funcion lambda. Lo detenemos por un segundo y el siguiente bucle pasara por todos los threads y los detendra mediante request_stop, lo cual llamara a la funcion de callback. Compilemos y veamos como es la salida:
$ ./threads
Thread id: 139830402217664; contar: 4
Thread id: 139830393824960; contar: 4
Thread id: 139830385432256; contar: 4
Thread id: 139830377039552; contar: 4
Thread id: 139830368646848; contar: 4
Thread id: 139830360254144; contar: 4
Thread id: 139830351861440; contar: 4
Thread id: 139830343468736; contar: 4
Thread id: 139830335076032; contar: 4
Thread id: 139830326683328; contar: 4
$
Por que contar es siempre 4? Porque cuando llamamos a func le suma un valor a contar, cuando el callback muestra a contar y el id le suma otro valor, y los dos valores restantes son las veces que lo detenemos tanto en la callback como en el main. Para poder pasar de un thread a otro se encarga request_stop en conjunto con el stop_token. Esto es una evolucion de la clase thread y como todo en este y otros lenguajes siempre deben adaptarlo o utilizarlo de acuerdo a la necesidad de su codigo.
En resumen, hoy hemos visto a jthread, que es, como se aplica, los beneficios que nos brinda, asi como tambien puede ser reemplazado mediante RAII, y un par de ejemplos para verlo en accion. Espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donation
It’s for maintenance of the site, thanks!
$1.50
