0

I found several tutorials online explaining how to update a QProgressBar during some long calculation. One of them is: use a QThread to do the calculation, then emit a signal that is connected to progressBar.setValue(int).

I thought this must also work for several QThread's that run at the same time, but something is not working correctly.

So, here is what I do: I want to calculate the trajectories of several particles (each with a long loop). To use multi-core processing, I create a QThread for each of these particles and let it call the respective calculation method. This works fine, all cores are used and the calculation finishes in roughly a quarter of time than before.

I wrote a Worker class based on this tutorial http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/. The header looks like this: (worker.h)

#include <world.h>

class Worker: public QObject
{
    Q_OBJECT

public:
    explicit Worker(World *world = 0, double deltaTau = 0., double maxDist = 0., double iterations = 0., double index = 0);

public slots:
    void process();

signals:
    void finished();
    void eror(QString err);

private:
    World *w;
    double my_deltaTau;
    double my_maxDist;
    int my_iterations;
    int my_index;
};

And the source like this: (worker.cpp)

#include "worker.h"

Worker::Worker(World *world, double deltaTau, double maxDist, double iterations, double index)
{
    w = world;
    my_deltaTau = deltaTau;
    my_maxDist = maxDist;
    my_iterations = iterations;
    my_index = index;
}

void Worker::process()
{
    w->runParticle(my_deltaTau, my_maxDist, my_iterations, my_index);
    emit finished();
}

Within world.cpp I have a function run that starts all the threading and the function runParticle that is called by the Worker:

void World::run(double deltaTau, double maxDist, int iterations)
{
    globalProgress = 0;
    for (int j = 0; j < particles->size(); j++) { //loop over all particles
        QThread *thread = new QThread;
        Worker *worker = new Worker(this, deltaTau, maxDist, iterations, j);
        worker->moveToThread(thread);
        connect(thread, SIGNAL(started()), worker, SLOT(process()));
        connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
        connect(worker, SIGNAL(finished()), thread, SLOT(deleteLater()));
        connect(thread, SIGNAL(finished()), worker, SLOT(deleteLater()));
        thread->start();
    }
}

void World::runParticle(double deltaTau, double maxDist, int iterations, int index)
{
    for (int i = 0; i < iterations; i++) { //loop over iteration steps
        if (i % 1000 == 0) { //only update the progress bar every 1000th iteration
            emit updateProgress(++globalProgress);
            qApp->processEvents(); // <--- I added this line, no effect!
        }
        [...] // <--- do my calculations for the particle's trajectories
    }
}

The public slot updateProgress(int) is called here every 1000th iteration step. It is connected to the QProgressBar in my MainWindow like this:

progressBar->setValue(0);
progressBar->setMaximum(nrPart * iter / 1000); //number of particles * number of iteration steps / 1000
connect(world, SIGNAL(updateProgress(int)), progressBar, SLOT(setValue(int)));
world->run(timeStep, dist, iter);

My problem is that the progress bar doesn't move until all the calculation is finished and then I see it move to 100% quite quickly.

Does anyone see my mistake or knows how to properly do this?

EDIT

I made the following changes:

(worker.h)

#include "world.h"

class Worker: public QObject
{
    Q_OBJECT

public:
    explicit Worker(World *world = 0, Particle *particle = 0, QList<MagneticField> *bfields = 0, double deltaTau = 0., double maxDist = 0., int iterations = 0);

public slots:
    void process();

signals:
    void finished();
    void updateProgress(int value);
    void ProcessParticle();
    void eror(QString err);

private:
    int i;
    Particle *p;
    QList<MagneticField> *magneticFields;
    double my_deltaTau;
    double my_maxDist;
    int my_iterations;
};

(worker.cpp)

#include "worker.h"

Worker::Worker(World *world, Particle *particle, QList<MagneticField> *bfields, double deltaTau, double maxDist, int iterations)
{
    i = 0;
    const World *w = world;
    p = particle;
    magneticFields = bfields;
    my_deltaTau = deltaTau;
    my_maxDist = maxDist;
    my_iterations = iterations;
    connect(this, SIGNAL(updateProgress(int)), w, SLOT(updateTheProgress(int)));
    connect(this, SIGNAL(ProcessParticle()), this, SLOT(process()), Qt::QueuedConnection);
}

void Worker::process()
{
    const int modNr = my_iterations / 1000;
    QDateTime start = QDateTime::currentDateTime();
    while (i < my_iterations) { //loop over iteration steps
        [...] // <--- do my calculations
        //handle progress
        emit updateProgress(1);
        if (QDateTime::currentDateTime() > start.addMSecs(300)) {
            emit ProcessParticle();
            ++i; //ensure we return to the next iteration
            return;
        }
        i++;
    }
    qDebug() << "FINISHED"; // <--- I can see this, so finished() should be emitted now...
    emit finished();
}

(part of world.h)

public slots:
    void threadFinished();
    void updateTheProgress(int value);

signals:
    void updateProgress(int value);

(part of world.cpp)

void World::threadFinished()
{
    particleCounter++;
    qDebug() << "particles finished: " << particleCounter; // <--- this is NEVER called !?!?
    if (particleCounter == particles->size()) {
        hasRun = true;
    }
}

void World::updateTheProgress(int value)
{
    globalProgress += value;
    emit updateProgress(globalProgress);
}

void World::run(double deltaTau, double maxDist, int iterations)
{
    globalProgress = 0;
    particleCounter = 0;
    hasRun = false;
    for (int i = 0; i < particles->size(); i++) { //loop over all particles
        QThread *thread = new QThread;
        Worker *worker = new Worker(this, &(*particles)[i], bfields, deltaTau, maxDist, iterations);
        worker->moveToThread(thread);
        connect(thread, SIGNAL(started()), worker, SLOT(process()));
        connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
        connect(worker, SIGNAL(finished()), thread, SLOT(deleteLater()));
        connect(worker, SIGNAL(finished()), this, SLOT(threadFinished())); // <--- this connection SHOULD make sure, I count the finished threads
        connect(thread, SIGNAL(finished()), worker, SLOT(deleteLater()));
        thread->start();
    }
}

(somewhere in MainWindow.cpp)

progressBar->setValue(0);
progressBar->setMaximum(nrPart * iter);
connect(world, SIGNAL(updateProgress(int)), progressBar, SLOT(setValue(int)));
world->run(timeStep, dist, iter);
while (!world->hasBeenRunning()) {} //wait for all threads to finish

As I marked in the code above, I never get notification when the threads have finished and I end up in an infinite loop in the MainWindow. Is there still something wrong with the World <-> Worker connections?

Bianfable
  • 237
  • 5
  • 13
  • 2
    The fact that the `Worker`, which lives in a separate thread, calls a method on the `World`, which lives in the main thread and tries to emit a signal, makes me nervous. The `updateProgress` signal really should be a signal on the worker, so that the `connect` call recognizes it to be a cross-thread signal. Not sure if this is the problem, but from what I know about Qt's threading model, your code isn't kosher. – Sebastian Redl Jun 11 '14 at 16:16
  • Your `World` and `Worker` classes seem very tightly coupled. Consider redesigning your `World` class so that it knows nothing about your `Worker` class and nothing about iterations. Move the iterations and the signal emission to the `Worker` class and then connect it to your progress bar. – RobbieE Jun 11 '14 at 17:11

2 Answers2

1

The problem is that in order for the emitted signal to be processed, you need to allow the event loop to run on the new thread. By remaining in the for loop in runParticle, that's not happening, until the function has finished.

There is a crude method to fix this, which is to call QApplication::processEvents every once in a while, during your loop.

A better method would be to redesign the object so that a number of iterations are processed, before exiting and allowing the event loop to run naturally.

So, to create a time slice for processing, in your for loop you'd time how long the iterations are taking. If the time has exceeded 1/30th of a second, then call a signal with type QueuedConnection to call your slot function again and exit the for loop.

The QueuedConnection will ensure that any events are processed and then your function is called again.

Assuming runParticle is a slot:-

void Worker::runParticle(...)
{
    static int i = 0;

    QDateTime start = QDateTime::currentDateTime();

    for(i; i < iteration; ++i)
    {
        // do processing


        emit updateProgress(++globalProgress);

        // check if we've been here too long
        if(QDateTime::currentDateTime() > start.addMSecs(300))
        {
          emit ProcessParticle(); // assuming this is connected to runParticle with a Queued Connection
          ++i; // ensure we return to the next iteration
          return;
        }
    }
}

On a different note, when an object is moved to a new thread, all its children are moved too, where a child is part of the QObject hierarchy.

By having the Worker object hold a pointer to the World, it's directly calling the World's runParticle function, which is still on the main thread. While this is unsafe, it also means that the runParticle function is being processed on the main thread.

You need to move the runParticle function to the Worker object, which is on the new thread.

TheDarkKnight
  • 27,181
  • 6
  • 55
  • 85
  • As you can see, I already tried with qApp->processEvents() and QApplication::processEvents() doesn't change anything in the loop. Your other suggestion sounds interesting, but I don't know how to make a Queued Connection!? – Bianfable Jun 11 '14 at 16:18
  • Qt::QueuedConnection is a parameter that you pass to call to connect. See http://qt-project.org/doc/qt-4.8/qobject.html#connect-3 – TheDarkKnight Jun 11 '14 at 16:21
  • So I make runParticle a slot and add a new signal ProcessParticle that is connected with that argument? And where do I connect these? – Bianfable Jun 11 '14 at 16:26
  • I've edited my answer. Please read below the example code. As for the connection, you'd make it directly in the Worker class constructor: connect(this, &Worker::ProcessParticles, this, &Worker:: runParticle, Qt::QueuedConnection); – TheDarkKnight Jun 11 '14 at 16:32
  • Ah, OK, but that is kinda annoying as the calculation needs to access lot's of stuff from a class Particle (that I haven't shown) of which I have a QList in World. I'll try to move that stuff into the Worker then... – Bianfable Jun 11 '14 at 16:41
  • 1. It is not necessary to specify `Qt::QueuedConnection` when making connections between objects living in different threads. Qt does it for you automatically. 2. Emission of a signal in a given thread does *not* require a running event loop in the same thread. That's the whole point of signals. You can emit them from native threads, via shims from C code, etc. Only the receiving objects need to live in a thread with running event loop *iff* the thread is different than the sender's thread. – Kuba hasn't forgotten Monica Jun 11 '14 at 18:50
  • @KubaOber, is this directed to me, or the OP? I specified QueuedConnection, because the object is sending a signal to itself and needs to return to process events before continuing. – TheDarkKnight Jun 12 '14 at 08:02
  • You connect the finished() slot to the thread's quit() slot. If this is called before threadFinished(), then there's no event loop to process the threadFinished function. I suggest you call threadFinished at the end of Process() and then emit finished() in threadFinished(). – TheDarkKnight Jun 12 '14 at 11:05
  • OK, I call threadFinished at the end of process() now and it works. Thanks for that ;) However, the progress bar still doesn't move until all threads have finished!?! – Bianfable Jun 12 '14 at 17:24
  • In Worker::process you have emit updateProgress(1); I don't think the constant argument of 1 is going to help here. – TheDarkKnight Jun 13 '14 at 07:45
  • The updateProgress in Worker is connected to updateTheProgress in World, which then counts up the globalProgress variable of World and emits an updateProgress with that globalProgress. Maybe this is why it doesn't work again because I take this detour with the World class, but how else would I know the globalProgress in the Worker (there are several threads running at the same time)? – Bianfable Jun 13 '14 at 08:10
1

IMO this is wrong. Creating a thread is costly and you want create a lot of them. First of all you should use QThreadPool, your case exactly matches functionality of this class.

There are also QtConcurrent methods which will reduce boiler plate code drastically (this is abandoned feature of Qt so it is recommended to use QThreadPool, but you can try it works quite well).

Marek R
  • 32,568
  • 6
  • 55
  • 140
  • Never heard about QThreadPool before, but it really sounds like what I should use here. So far, I "only" simulate 100 particles - i.e. creating 100 Threads at a time - and it doesn't yet crash. If I try 1000 particles, it crashes instantly. Hope QThreadPool solves this... – Bianfable Jun 12 '14 at 17:30
  • @Bianfable, I just noticed this. The problem here is not the use of QThread, but how you are using it. You don't need more threads than processor cores and will get no benefit from this. Don't create one thread per particle. Instead, move multiple particle worker objects to one thread. – TheDarkKnight Jun 13 '14 at 09:08
  • OK, I rewrote basically all my code now to do the following: from MainWindow I call world->run(...), which then uses QtConcurrent::map(...) to start my calculation in the *correct* number of threads. Then I use QFutureWatcher to get the progress and show it in a QProgressDialog as described here: http://www.bogotobogo.com/Qt/Qt5_QtConcurrent_QFutureWatcher_QProgressDialog_map.php . It somehow still crashes, but that doesn't belong here as the progress bar issue is solved! – Bianfable Jun 13 '14 at 11:19