lunes, 30 de enero de 2017

C++ - Threads (Hilos)

Threads (Hilos)

En esta entrada, se hablará sobre los Threads de la librería estándar de C++11. Se verá lo básico para poder empezar a utilizarlos.




Empecemos con una introducción: ¿Qué son los threads, o hilos? A la hora de crear un programa, generalmente nos encontraremos con que nuestro programa ejecuta las instrucciones en orden, una por una. Esto es correcto, hasta el momento en que necesitamos hacer varias tareas a la vez, ya sea por necesidad, por acelerar un proceso, o cualquier otra razón que se nos presente. Es en este punto, en que podemos utilizar los llamados "hilos" ("threads", por su nombre en inglés, y que usaré a partir de ahora).
Un thread se ejecutará de forma paralela a nuestro programa principal, ejecutando las mismas instrucciones u otras. Un thread tiene un comienzo (normalmente, cuando es creado), y tiene un final, que suele ser cuando ha terminado su propósito. Es posible tener todos los threads que se deseen a la vez. La única limitación será la que pudiese poner el Sistema Operativo y la lógica. También hay que tener en cuenta, que no por tener más threads van a ir los procesos necesariamente más rápido.

Antes de empezar con ellos, una anotación más: Los threads pueden compartir recursos. Por ejemplo, en C++, si tenemos una variable global y lanzamos un thread, ese thread puede acceder también a ella. Es importante tener en cuenta que si dos threads acceden a la vez al mismo recurso (especialmente si lo van a modificar), se pueden generar un comportamiento "aleatorio" ya que quizás un thread lo modifique primero y luego el otro, o viceversa. Lo mismo va para funciones y clases que trabajen con streams, como es hacer "cout << var;". Esto no lo voy a extender en esta entrada, pero aviso de forma anticipada de posibles problemas que pudiera ocurrir.

Dicho esto, vamos al código. Utilizaremos la librería <thread>, y de esta, la clase std::thread. La clase thread, tras ser construida, generará un thread, sin necesidad de llamar a ningún método suyo. Digo esto, porque en otros lenguajes, como Java, nos podemos encontrar con que haya que llamar a algún método suyo para iniciarlo.

El constructor de thread que utilizaremos, recibe de primer argumento, la función que ejecutará. Véase que los threads ejecutarán una función. Cuando termine la función, el thread terminará.

#include <iostream>
#include <thread>

using namespace std;

void func(){
}

int main(){
    thread th(func);
}


Si la función que llama el thread tiene parámetros, se los podemos pasar también. Para ello, usaremos el mismo constructor, pero agregaremos los parámetros al final.
#include <iostream>
#include <thread>

using namespace std;

void func(int n, double m){
}

int main(){
    thread th(func, 1, 5.7);
}


Es probable que al ejecutar estos códigos, el programa termine con un error. Esto ocurre si hay threads trabajando cuando se termina el programa (se termina la función main).
Para solucionar esto, hay 2 opciones. La primera, y la más recomendable generalmente, es esperar a que los threads terminen. Para ello, tenemos 2 métodos: join() y joinable(). Join espera hasta que el thread termine. Joinable nos dice si podemos hacer join a ese thread.

#include <iostream>
#include <thread>

using namespace std;

void func(int n, double m){
    cout << n << " " << m << endl;
}

int main(){
    thread th(func, 1, 5.7);
    
    if(th.joinable()) {
        th.join();
    }
}


La otra opción, es llamar al método detach(). Con ello, el thread deja de ser joinable. Además, ese thread no dará error si el programa termina mientras el thread está trabajando. Sin embargo, el thread será interrumpido bruscamente, cosa que no nos interesa.


Ahora que hemos visto lo básico, veamos algunos detalles.
Los parámetros de la función que le pasemos al thread pueden ser de cualquier tipo: tipos nativos, objetos, punteros, referencias... El único detalle, es que una referencia no se pasa de forma trivial. Hay que utilizar la función std::ref():

#include <iostream>
#include <chrono>
#include <thread>

using namespace std;

void func(bool& empezar){
    while(!empezar){
        this_thread::sleep_for(chrono::milliseconds(1));
    }
    cout << "B" << endl;
}

int main(){
    bool empezar = false;
    thread th(func, ref(empezar));
    
    cout << "A" << endl;
    
    empezar = true;
    
    if(th.joinable()) {
        th.join();
    }
}


En este ejemplo, el thread esperará hasta que nuestra variable "empezar" se ponga a true.
La línea this_thread::sleep_for(chrono::milliseconds(1)); lo único que hace es esperar 1 milisegundo (para así evitar sobrecargar la CPU) (Los bucles vacíos como el del ejemplo suelen consumir bastante CPU).
Por supuesto, tal como he modificado "empezar" desde el main, también se puede modificar desde el thread. La memoria de la variable, es la misma. También se podría hacer con una variable global, aunque no recomiendo tener muchas variables globales en los programas.


Y aquí termina esta entrada. Hay muchos temas que ver sobre los threads, pero esos los examinaré en siguientes entradas. Si esta entrada os da la información suficiente para hacer algún programa con varios threads, habrá cumplido su propósito.

2 comentarios:

  1. Mi nombre es Marcos, leí toda tu entra, quería saber si podrías hacer una entrada que explique algo de Sockets en c++... siendo que tus entradas son muy fáciles de entender.

    Gracias!

    ResponderEliminar
    Respuestas
    1. Buenas Marcos!
      Sí, las próximas entradas que haga, intentaré que sean orientadas a Sockets.
      Como habrás podido ver, no soy muy activo subiendo entradas (últimamente tengo muchas cosas en la cabeza).
      Con suerte, las subo en poco tiempo. Con mala suerte, cuando la suba, te habrás olvidado ya de que el blog existe :X
      Pero... lo apunto como TO-DO!
      Y gracias por el feedback :)!

      Eliminar