Parallel programming using QtConcurrent.
With computers getting more powerful, data computation is getting expensive. Many a times we need to perform heavy computations using multi threading to distribute this computing load. In C++ we use std::thread
or std::async
to achieve multi threading. In Qt5 we have QThread
or QtConcurrent
to achieve the same.
According to QtConcurrent docs:
The QtConcurrent namespace provides high-level APIs that make it possible to write multi-threaded programs without using low-level threading primitives such as mutexes, read-write locks, wait conditions, or semaphores.
In simpler words, QtConcurrent
provides functionality to spawn threads without explicit interaction from the user. In addition to this, we also have possibility to use signals
and slots
across the threads.
In this tutorial we will see how to run a function asynchronously using QtConcurrent
and also use signals
and slots
to update GUI from the thread created by QtConcurrent
.
While performing computationally expensive tasks, GUI becomes unresponsive or laggy because of the reason that the GUI/Main thread is blocked by a heavy task but at the same time cannot keep the GUI responsive.
We need multi threading to keep our GUI responsive while performing computationally expensive tasks.
Here we make a small application with Start
and Stop
buttons. Start
button spawns a new thread and Stop
button kills the thread.
In the layout we have a QLCDNumber
and QProgressBar
as a reference to see how the GUI is updated.
Lets have look at our Dialog.cpp:
#include <QtConcurrent/QtConcurrent>
#include <QFuture>
#include "dialog.h"
#include "ui_dialog.h"
Dialog::Dialog(QWidget* parent)
: QDialog(parent)
, ui(new Ui::Dialog)
{
ui->setupUi(this);
m_worker = new Worker(this);
// connections
connect(m_worker, &Worker::updateGUI, this, &Dialog::onUpdateGUI);
connect(this, &Dialog::stop, m_worker, &Worker::stop);
connect(ui->pushButton, &QPushButton::clicked, this, &Dialog::onStartClicked);
connect(ui->pushButton_2, &QPushButton::clicked, this, &Dialog::onStopClicked);
}
Dialog::~Dialog()
{
// stop the thread before closing the application
// otherwise, the window will be destroyed but
// the thread will continue running
emit stop();
delete ui;
}
void Dialog::onStartClicked()
{
// prints current thread id
// here it is the main thread
qDebug() << "Main thread: " << this->thread()->currentThreadId();
// create an concurrent thread
QFuture<void> future = QtConcurrent::run(m_worker, &Worker::asyncFunction);
ui->pushButton->setEnabled(false);
}
void Dialog::onStopClicked()
{
emit stop();
ui->pushButton->setEnabled(true);
}
void Dialog::onUpdateGUI(int value)
{
// update lcd number and progress bar
ui->lcdNumber->display(value);
ui->progressBar->setValue(value);
}
We declare 3 slots OnStartClicked()
, OnStopClicked()
, OnUpdateGUI()
and 1 signal stop()
.
We have a Worker
class which has a function asyncFunction()
and this function in our tutorial will run on a different thread.
Implementation looks like this:
#include <QThread>
#include "worker.h"
Worker::Worker(QObject* parent) :
QObject(parent)
, m_stop(false)
{
}
void Worker::stop()
{
m_stop = true;
}
void Worker::asyncFunction()
{
// prints worker thread id
qDebug() << "Worker thread: " << this->thread()->currentThreadId();
m_stop = false;
for(int i = 0; i <= 100; i++)
{
// if stop requested
// break this loop
if(m_stop)
{
return;
}
// signal dialog class to update GUI
emit updateGUI(i);
// thread sleep for 50 ms
// otherwise this is too fast
this->thread()->msleep(50);
}
}
To make this work, we need proper connections like the ones we have defined in Dialog.cpp.
connect(m_worker, &Worker::updateGUI, this, &Dialog::onUpdateGUI);
connect(this, &Dialog::stop, m_worker, &Worker::stop);
First connection ensures that every time the worker class emits a signal updateGUI
, the slot on onUpdateGUI
is called in Dialog
class.
By default, a connect statement in Qt looks something like this:
QObject::connect(const QObject *sender, const char *signal, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
The last parameter Qt::AutoConnection
is the key here. Qt automatically decides to use Qt::QueuedConnection
while connecting signals
and slots
across the threads. We can also specify explicitly to use Qt::QueuedConnection
inside our connect statements.
Our second connect statement is for stopping the thread. Notice we should always use signals and slots when working with multi threading since Qt handles most of the complications associated with it automatically.
Point of interest here is QtConcurrent::run()
. It spawns a thread every time Start
button is clicked and our function asyncFunction()
runs asynchronously on a different thread.
To test it, we print the thread id:
Main thread: 0x4d48
Worker thread: 0x39cc
Running complex and heavy tasks on different threads not only distributes the load and makes the program run faster but also keeps GUI/Main thread free, which helps in keeping our UI responsive.
Here is our demo project output in action:
The whole project can be found on my GitHub account. If this was helpful, please share this blog and also feel free to add your thoughts or comments below.
Photo by drmakete lab on Unsplash