Jolouster
Come, bebe y ve el bien por todo tu duro trabajo

Crear, eliminar y volver a crear hilos en Qt/C++

Autor Jonathan López - February 4, 2019

Estoy haciendo pruebas con C++ y Qt. Quiero hacer un programa que según los datos que encuentre en la base de datos cree X hilos para realizar cada uno de ellos un proceso laaaaargo. Este proceso se debe repetir indefinidamente mientras el usuario no lo detenga.

Para ello tiene dos opciones:

  • Detener el proceso temporalmente. Típico “start/stop”.
  • Reiniciarlos.

Reiniciarlos tiene sentido cuando el usuarios ha modificado los datos almacenados en la base de datos o archivos (la configuración).

Es en esta opción en la que me voy a centrar en este artículo.

Si lo reinicia quiero eliminar todo. Eliminar los hilos creados de forma dinámica y los objetos que se han movido a estos hilos para realizar el trabajo. Después habría que volver a empezar el proceso desde cero.

Se que se podría decir al usuarios que cerrara la aplicación y volviera ha ejecutarla, pero no sería elegante. Quireo aprender a hacerlo “bien” y de paso lo comparto con todos vosotros.

Este es el código que empleo para las pruebas:

Archivo myWorker.h

En este archivo tengo la declaración de la clase que se encarga de hacer “el trabajo duro” en cada hilo o thread.

    #ifndef MYWORKER_H
    #define MYWORKER_H

    #include <QCoreApplication>
    #include <QObject>
    #include <QDebug>

    namespace jlu
    {
        class myWorker : public QObject
        {
                Q_OBJECT

            public:
                explicit myWorker ();
                ~myWorker ();
                void setWorkerID (int workID);
            signals:
                void startProcess();
            public slots:
                void changeStatus (int id, bool newStatus);
                void process();    // <---- Aquí dentro se ejecutará todo lo que nos interesa.

            private:
                void loadConfigData();

                // Propiedades de la clase:
                bool isActive;
                int myID;
        };
    }


    #endif // MYWORKER_H

Archivo myWorker.cpp

La implementación de la clase.

    #include "myworker.h"

    jlu::myWorker::myWorker()
    {
        connect (this, SIGNAL (startProcess()),
                this, SLOT (process()));
    }

    jlu::myWorker::~myWorker()
    {
        // Mensaje que uso para comprobar si se ha eliminado o no.
        qDebug() << "Se ha eliminado al hilo con ID: " << myID;
    }

    void jlu::myWorker::setWorkerID (int workID)
    {
        if (0 <= workID)
        {
            myID = workID;
        }
        else
        {
            myID = 0;
        }
    }

    void jlu::myWorker::changeStatus (int id, bool newStatus)
    {
        if (myID != id)
        {
            return;
        }

        isActive =  newStatus;

        if (isActive)
        {
            emit startProcess();
        }
    }

    void jlu::myWorker::process()
    {
        loadConfigData(); // Carga toda la información necesaria para funcionar.
        qDebug() << "Se inicia el proceso con id = " << myID;

        while (isActive)
        {
            // Un laaargo proceso que se repita indefinidamente

            // Aquí va el código que quiero que se ejecute indefinidamente hasta
            // que el usuario diga basta.
            
            // Atendemos los eventos que se hayan podido producir.
            QCoreApplication::processEvents (QEventLoop::AllEvents);
        }

        qDebug() << "Proceso detenido.";
    }

    void jlu::myWorker::loadConfigData()
    {
        // Carga datos desde la base de datos.
        // Carga datos de archivos de configuración etc.
    }

Archivo manageWorkers.h

En este archivo tengo la declaración de la clase encargada de iniciar, controlar los “threads” y detenerlos cuando el usuario diga.

    #ifndef MANAGEWORKERS_H
    #define MANAGEWORKERS_H

    #include <QObject>
    #include <QThread>
    #include <QDebug>
    #include "myworker.h"

    namespace jlu
    {
        class manageWorkers : public QObject
        {
                Q_OBJECT
            public:
                explicit manageWorkers (QObject * parent = nullptr);
                ~manageWorkers();
            signals:
                void changeStatusOfWorker (int id, bool newStatus);
            public slots:
                void loadAndStartAllWorkers();
                void killallWorkers();
            private:
                int numWorkers;
                QThread * myThreads = NULL;
                myWorker * myProcess = NULL;
        };
    }
    #endif // MANAGEWORKERS_H

Archivo manageWorkers.cpp

Su implementación.

    #include "manageworkers.h"

    jlu::manageWorkers::manageWorkers (QObject * parent) : QObject (parent)
    {

    }

    jlu::manageWorkers::~manageWorkers() {}


    void jlu::manageWorkers::loadAndStartAllWorkers()
    {
        numWorkers = 4; // Esto es realmente el resultado de una "futura" consulta SQL

        if (0 == numWorkers)
        {
            return;
        }

        myThreads = new QThread[numWorkers];
        myProcess = new jlu::myWorker[numWorkers];

        for (int i = 0; i < numWorkers; i++)
        {
            myProcess[i].setWorkerID (i); // Se tomará de la consulta realizada
            myProcess[i].moveToThread (&myThreads[i]);

            connect (this, SIGNAL (changeStatusOfWorker (int, bool)),
                    &myProcess[i], SLOT (changeStatus (int, bool)));
            // Demás conexiones necesarias

            myThreads[i].start();
            // Iniciamos el proceso.
            emit changeStatusOfWorker (i, true);
        }
    }

    void jlu::manageWorkers::killallWorkers()
    {
        qDebug() << " keys: " << netsConfigList.keys();

        for (int i = 0; i < numWorkers; i++)
        {
            qDebug() << "Se detiene el hilo n. " << i;
            emit changeStatusOfWorker (i, false);
            myThreads[i].quit();         // <---  El orden importa!! 
            myThreads[i].wait (1000);
        }

        // Ahora se eliminan los objetos y se libera la memoria correctamente.
        delete []myThreads;
        delete []myProcess;

        qDebug() << " Se han eliminado todos los hilos";
    }

Aquí es donde se produce el control de los hilos. Su inicio, gestión y eliminación si así lo deseamos.

En el método jlu::manageWorkers::loadAndStartAllWorkers() es donde se crean tantos “hilos” como hagan falta y los objetos de la clase “myWorker” que quiero ejecutar en cada hilo.

Según muchos ejemplos y documentación en esta parte es donde también se añade la siguiente conexión:

connect (&myThreads[i], SIGNAL (finished()), &myProcess[i], SLOT (deleteLater()));

Si añadimos esta conexión, no funcionará. De echo el programa se romperá. Tras hacer myThreads[i].quit(); se lanzaría la señal que le diría al objeto myProcess[i] que se destruyese cuando pudiese… Esto parece que deja al objeto myProcess[i] medio colgado por ahí y da el fallo que comentaba.

Archivo main.cpp para las pruebas

El siguiente archivo es donde pruebo el funcionamiento del codigo anterior. Simulo en envío las señales de inicio y paro que podría lanzar el usuario pulsando un botón de la interfaz gráfica.

    #include <QCoreApplication>
    #include <QTimer>
    #include <QObject>
    #include <QDebug>
    #include <QThread>
    #include <QDateTime>

    // Inserción de mis archivos.
    #include "manageworkers.h"

    #define DATETIME_FORMAT "yyyy-MM-dd HH:mm:ss"


    // Programa principal solo para testear el reinicio de los objetos.
    int main (int argc, char * argv[])
    {
        QCoreApplication a (argc, argv);

        jlu::manageWorkers * myControl = new jlu::manageWorkers();
        /*
         * El programa inicializa automáticamente los hilos y los procesos. Se ha elegido así.
         */
        myControl->loadAndStartAllWorkers();
        qDebug () << QDateTime::currentDateTime().toString (DATETIME_FORMAT);

        /*
         * A continuación se simula el envío de las señales de inicio y detención que podría 
         * lanzar el usuario desde la interfaz gráfica.
         */

        // Tras 10 segundos de funcionamiento detengo todos los hilos
        QTimer::singleShot (10000, myControl, SLOT (killallWorkers()));
        
        // Tras 5 segundos de la detención los vuelvo a iniciar
        QTimer::singleShot (15000, myControl, SLOT (loadAndStartAllWorkers()));

        // Tras 7 segundos del ultimo reinicio de los hilos, los vuelvo a eliminar
        QTimer::singleShot (23000, myControl, SLOT (killallWorkers()));

        /*
         * ---- fin de las pruebas ----
         */

        // Finalizamos el programa tras N seg.
        QTimer::singleShot (25000, &app, SLOT (quit()));

        return a.exec();
    }

¿Pensáis que se podría hacer de alguna otra manera más simple o mejor? ¿Qué opináis de este ejemplo? ¿Tenéis alguna duda?

No dudéis en comentar.

P.D.: Si quisiéras simular el inicio/pausa del proceso que se ejecuta en los “hilos” sin eliminarlos, probad a lanzar la señal changeStatusOfWorker() de la clase manageWorkers.


Te invito a que dejes un comentario más abajo y me digas que opinas al respecto.