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.