Callback for a multithreaded GUI

In a previous post I showed a brilliant little multithreaded application where two threads, driven by two buttons, were competing on the same resource, a label displaying a number that changed accordingly to the requests of the competing threads.

That application was kind of elegant, using the Qt slot mechanism to connect the working threads with the main window GUI. Alas it was wrong, too. You could not see the error in such a simple application that apparently works correctly, but the point is that simply using "emit" in a thread to connect to another function is not enough to make this other function to run in the same thread. What happened there was that the called function was running in the same thread (the main application thread) whatever was the calling thread. Not exactely what was expected.

So far about the bad news. Good news is that is not a big issue rewriting the code so that it works as I wanted. Here we see how to do it using the good old C++ callback mechanism for classes. By the way, if you need to call just a free function from different threads, you could have a look at another post, that shows exactely that.

The basic idea is that our increasers should know how to call from their run() function - the actual function that runs in an own specific thread - the function in MainWindow that modifies the label. We can easily get this passing to the class a pointer to the MainWindow object itself:
#ifndef INCREASER_H
#define INCREASER_H

#include <QThread>

class MainWindow; // 1.

class Increaser : public QThread
{
public:
Increaser(int step);
void run();
void stop();
void restart();

static void setCallback(MainWindow* that); // 2.
private:
bool isRunnable_;
int step_;

static MainWindow* that_; // 3.
};

#endif // INCREASER_H

1. We don't need to know all the MainWindow class details at this point, we just need to know that we are talking about a class.
2. I assume that we want all the increasers working on the same instance of MainWindow (quite reasonable in this case), so the setCallback() method is static, it has to be called just once for all the increasers.
3. As said above, all the increasers share a reference to the same MainWindow object.

The implementation code for the Increaser class does not change much, but I report it here all of it, just fof clarity sake:
#include "increaser.h"
#include "mainwindow.h" // 1.
#include <iostream>

MainWindow* Increaser::that_ = NULL; // 2.

void Increaser::setCallback(MainWindow* that) { that_ = that; } // 3.

Increaser::Increaser(int step) : isRunnable_(true), step_(step) {}

void Increaser::run()
{
while(true)
{
std::cout << "Step: " << step_ << " in "
<< this->currentThreadId() << std::endl;
msleep(500);
if(!isRunnable_)
break;
if(that_) // 4.
that_->changeValue(step_);
}
}

void Increaser::stop()
{
isRunnable_ = false;
wait();
}

void Increaser::restart()
{
isRunnable_ = true;
QThread::start();
}

1. Here we need the class details, so that the compiler could actually check that really MainWindow has a public method as the one we claim that we want to use.
2. that_ is a static member of the Increaser class, so we have to define it somewhere in the code. Here is a good place. Notice that we explicitely initialize it to NULL: no MainWindows object is available if not set by someone else.
3. And here is the static method that initialize the Increaser static member.
4. The MainWindow changeValue() method is called only if we actually have a valid reference to an object of that type.

The changes in the MainWindow class are even lighter ones.

In the class declaration the method changeValue() is not anymore defined as a slot, but as a "normal" public member:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

// ...

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
// ...

void changeValue(int);

private slots:
// ...
};

#endif // MAINWINDOW_H

The class constructor now sets the callback for the increasers:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow),
increaser_(2), decreaser_(-3), curValue_(100)
{
ui->setupUi(this);
ui->lbValue->setText(QString::number(curValue_));

Increaser::setCallback(this);
}

And here is again the function called by the incresers:
void MainWindow::changeValue(int step)
{
QMutexLocker ml(&mCV_);

std::cout << "change value in " << QThread::currentThreadId() << std::endl;
ui->lbValue->setText(QString::number(curValue_ += step));
}

Running the application we can now see how the thread id is the one of the calling thread. So we can finally say that we have got what we wanted.

No comments:

Post a Comment