Multithreading with a GUI

Have you ever dreamt of a tiny Qt window-based application showing a number that could be continuously increased or decreased pushing on a couple of buttons? Well, that is what we are about to do in this post.

OK, it's a pretty silly application, but it has a few good points to show. Through it we could easily show how the user could stop and start other threads from the main one, and we could see the connect mechanism as a way to allow us to easily establish a communication between different threads.

I leave out the creation of the application graphical part, think of it as a preparatory exercise. As a hint, I tell you that I created by Qt Creator a Qt Gui Application with a QMainWindow. Then, in design mode, I put in the window a QLabel (named lbValue), and a couple of QPushButton objects, btnIncrease and btnDecrease. I have even embellished a bit the mainWindow, adding another QLabel saying "Current value:" and other few details that here have no primary interest (an Exit button, a few menu items ...).

Then I created a connection from each of the two buttons to a slot, right clicking on each of them and selecting the "Go to slot..." item in the popup window, selecting clicked() as signal. In response to that, Qt created a couple of private slots in the MainWindow class declaration, named on_btnDecrease_clicked() and on_btnIncrease_clicked() that are connected as expected to the proper button.

Now the fun stuff.

Actually, what we are going to see here is a Qt antipattern. Using a QThread derived class in this way is attractive but wrong. A working alternative is shown in another post, where I use a callback mechanism in multithreading environment that works right.

We want that clicking on a button a thread starts and modify the label value adding (or subtracting) a specific amount. To do that we can create a class like this:
#ifndef INCREASER_H
#define INCREASER_H

#include <QThread>

class Increaser : public QThread // 1
{
Q_OBJECT // 2
public:
  Increaser(int step); // 3
  void run(); // 4
  void stop(); // 5
  void restart(); // 6
signals:
  void increase(int); // 7
private:
  bool isRunnable_; // 8
  int step_;
};

#endif // INCREASER_H
1. We are extending QThread, as we have already seen previously.
2. We need the Q_OBJECT macro, since we want to use the signal-slot connection mechanism.
3. The ctor requires a step, that we'll use to increment the label value
4. Override of run(), virtual function defined in QThread.
5. Terminates the execution of the current thread.
6. When we want to restart an Increaser that has already been started, we should call this specific method.
7. When the current thread wants the label to be changed emits this signal. If this signal is not connected to any slot, nothing happens.
8. Flag that we use to gracefully stop (and restart) the current thread.

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

void Increaser::run()
{
  while(true) // 1
  {
    msleep(500); // 2
    if(!isRunnable_) // 3
      break;
    emit increase(step_); // 4
  }
}

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

void Increaser::restart()
{
  isRunnable_ = true; // 6
  QThread::start();
}
1. When the current thread runs, it executes this infinite loop.
2. It sleeps for half a second.
3. If a request to stop the thread has been issued, the loop breaks - so the run terminates.
4. If the run is still active, an increase signal is emitted. [Dammit, this does not work as expected - at least, as expected by me. The connect mechanism does not let this code being executed by the current thread, but transfer the request to the main thread. If what you need to call at this point is just a free function, you could have a look at this other post where I use a function pointer as a callback. A more general solution is showed in another post, using a C++ class callback mechanism].
5. The stop() call is synchronous. The run flag is set to false, then we wait till run() terminates, before returning to the caller.
6. If we don't reset the run flag to true, run() would immediately terminate.

Now we are ready to extend our MainWindow:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QMutex>
#include "increaser.h"

namespace Ui
{
  class MainWindow;
}

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

  void closeEvent(QCloseEvent *); // 1
private slots:
  void on_btnDecrease_clicked(); // 2
  void on_btnIncrease_clicked();
  void changeValue(int); // 3
private:
  Ui::MainWindow *ui;
  Increaser increaser_; // 4
  Increaser decreaser_;

  int curValue_; // 5
  QMutex mCV_; // 6
};

#endif // MAINWINDOW_H
1. We don't want to leave the execution when secondary threads are still running, so we ensure here they are terminated.
2. The two slots created by Designer accordingly to our request.
3. The slot we create to connect to the Increaser signal (notice that this would lead to troubles).
4. A couple of Increaser objects.
5. The current value that is displayed in the label.
6. We'll have two threads competing for changing curValue_, so we should rule its access using a QMutex.

Here is the ctor definition:
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_)); // 1

  connect(&increaser_, SIGNAL(increase(int)), this, SLOT(changeValue(int))); // 2
  connect(&decreaser_, SIGNAL(increase(int)), this, SLOT(changeValue(int)));
}
1. The label text is set using the starting value given to curValue_.
2. We connect the signal of both Increaser objects to the local slot. They are competing on the same resource. [This looks quite cool, but it does not work as I expected. The connection reside in the current (main) thread, and not in the one managed by the Increaser objects. An alternative approach could be using function pointers. Or we can modify this code using C++ class callback instead.]

I show just one on_xxx_clicked() slot, since they are essentially the same. The difference is that one refers to increaser_ and btnIncrease, the other to decreaser_ and btnDecrease:
void MainWindow::on_btnIncrease_clicked()
{
  if(increaser_.isRunning()) // 1
  { // the thread is running, stop it
    ui->btnIncrease->setText("Start +");
    increaser_.stop();
  }
  else // 2
  { // start (or restart) the thread
    ui->btnIncrease->setText("Stop +");
    increaser_.restart();
  }
}
1. If the thread is already running, clicking the button we ask to stop it.
2. Otherwise we (re)start it. Notice that I just call restart() without caring much of the difference. It would make sense calling explicitly start() only when we wanted to assign a specific priority to the thread.

When a request to close the main window is issued, we ensure the Increaser thread are stopped before terminating:
void MainWindow::closeEvent(QCloseEvent *)
{
  increaser_.stop();
  decreaser_.stop();
}
Here is the slot that lets the Increasers modify the label value:
void MainWindow::changeValue(int step)
{
  QMutexLocker ml(&mCV_);
  ui->lbValue->setText(QString::number(curValue_ += step));
}
Here the code is quite simple, and we shouldn't have much harm even if both thread would try to run it concurrently. Worst case scenario is that both increase on curValue_ happen before the label is actually displayed, so we could lose to see a number that was doomed to disappear in moment. In any case, using a QMutexLocker we ensure it working correctly, without losing anything at all. [Actually, currently here the mutex has no use at all, since this function is not called by different threads, as I expected. See an alternative using function pointers and another, more to the point, that uses C++ object callback]

No comments:

Post a Comment