Multithreading with Qt

1
16066
13 min read

Qt has its own cross-platform implementation of threading. In this article by Guillaume Lazar and Robin Penea, authors of the book Mastering Qt 5, we will study how to use Qt and the available tools provided by the Qt folks.

(For more resources related to this topic, see here.)

More specifically, we will cover the following:

  • Understanding the QThread framework in depth
  • The worker model and how you can offload a process from the main thread
  • An overview of all the available threading technologies in Qt

Discovering QThread

Qt provides a sophisticated threading system. We assume that you already know threading basics and the associated issues (deadlocks, threads synchronization, resource sharing, and so on) and we will focus on how Qt implements it.


The QThread is the central class for of the Qt threading system. A QThread instance manages one thread of execution within the program.

You can subclass QThread to override the run() function, which will be executed in the QThread class. Here is how you can create and start a QThread:

QThread thread;
thread.start();

The start() function calling will automatically call the run() function of thread and emit the started() signal. Only at this point, the new thread of execution will be created. When run() is completed, thread will emit the finished() signal.

This brings us to a fundamental aspect of QThread: it works seamlessly with the signal/slot mechanism. Qt is an event-driven framework, where a main event loop (or the GUI loop) processes events (user input, graphical, and so on) to refresh the UI.

Each QThread comes with its own event loop that can process events outside the main loop. If not overridden, run() calls the QThread::exec() function, which starts the thread’s event loop. You can also override QThread and call exec(), as follows:

class Thread : public QThread
{
Q_OBJECT
protected:
    void run() 
    {
      Object* myObject = new Object();
        connect(myObject, &Object::started, 
                this, &Thread::doWork);
        exec();
    }

private slots:
    void doWork();
};

The started()signal will be processed by the Thread event loop only upon the exec() call. It will block and wait until QThread::exit() is called.

A crucial thing to note is that a thread event loop delivers events for all QObject classes that are living in that thread. This includes all objects created in that thread or moved to that thread. This is referred to as the thread affinity of an object. Here’s an example:

class Thread : public QThread
{
    Thread() :
        mObject(new QObject())
    {
    }
private :
    QObject* myObject;
};

// Somewhere in MainWindow
Thread thread;
thread.start();

In this snippet, myObject is constructed in the Thread constructor, which is created in turn in MainWindow. At this point, thread is living in the GUI thread. Hence, myObject is also living in the GUI thread.

An object created before a QCoreApplication object has no thread affinity. As a consequence, no event will be dispatched to it.

It is great to be able to handle signals and slots in our own QThread, but how can we control signals across multiple threads? A classic example is a long running process that is executed in a separate thread that has to notify the UI to update some state:

class Thread : public QThread
{
    Q_OBJECT
    void run() {
        // long running operation
        emit result("I <3 threads");
    }
signals:
    void result(QString data);
};

// Somewhere in MainWindow
Thread* thread = new Thread(this);
connect(thread, &Thread::result, this, &MainWindow::handleResult);
connect(thread, &Thread::finished, thread, &QObject::deleteLater);
thread->start();

Intuitively, we assume that the first connect function sends the signal across multiple threads (to have a result available in MainWindow::handleResult), whereas the second connect function should work on thread’s event loop only.

Fortunately, this is the case due to a default argument in the connect() function signature: the connection type. Let’s see the complete signature:

QObject::connect(
    const QObject *sender, const char *signal, 
    const QObject *receiver, const char *method, 
    Qt::ConnectionType type = Qt::AutoConnection)

The type variable takes Qt::AutoConnection as a default value. Let’s review the possible values of Qt::ConectionType enum as the official Qt documentation states:

  • Qt::AutoConnection: If the receiver lives in the thread that emits the signal, Qt::DirectConnection is used. Otherwise, Qt::QueuedConnection is used. The connection type is determined when the signal is emitted.
  • Qt::DirectConnection: This slot is invoked immediately when the signal is emitted. The slot is executed in the signaling thread.
  • Qt::QueuedConnection: The slot is invoked when control returns to the event loop of the receiver’s thread. The slot is executed in the receiver’s thread.
  • Qt::BlockingQueuedConnection: This is the same as Qt::QueuedConnection, except that the signaling thread blocks until the slot returns. This connection must not be used if the receiver lives in the signaling thread or else the application will deadlock.
  • Qt::UniqueConnection: This is a flag that can be combined with any one of the preceding connection types, using a bitwise OR element. When Qt::UniqueConnection is set, QObject::connect() will fail if the connection already exists (that is, if the same signal is already connected to the same slot for the same pair of objects).

When using Qt::AutoConnection, the final ConnectionType is resolved only when the signal is effectively emitted. If you look again at our example, the first connect():

connect(thread, &Thread::result, 
        this, &MainWindow::handleResult);

When the result() signal will be emitted, Qt will look at the handleResult() thread affinity, which is different from the thread affinity of the result() signal. The thread object is living in MainWindow (remember that it has been created in MainWindow), but the result() signal has been emitted in the run() function, which is running in a different thread of execution. As a result, a Qt::QueuedConnection function will be used.

We will now take a look at the second connect():

connect(thread, &Thread::finished, thread, &QObject::deleteLater);

Here, deleteLater() and finished() live in the same thread, therefore, a Qt::DirectConnection will be used.

It is crucial that you understand that Qt does not care about the emitting object thread affinity, it looks only at the signal’s “context of execution.”

Loaded with this knowledge, we can take another look at our first QThread example to have a complete understanding of this system:

class Thread : public QThread
{
Q_OBJECT
protected:
    void run() 
    {
        Object* myObject = new Object();
        connect(myObject, &Object::started, 
                this, &Thread::doWork);
        exec();
    }

private slots:
    void doWork();
};

When Object::started() is emitted, a Qt::QueuedConnection function will be used. his is where your brain freezes. The Thread::doWork() function lives in another thread than Object::started(), which has been created in run(). If the Thread has been instantiated in the UI Thread, this is where doWork() would have belonged.

This system is powerful but complex. To make things more simple, Qt favors the worker model. It splits the threading plumbing from the real processing. Here is an example:

class Worker : public QObject
{
    Q_OBJECT
public slots:
    void doWork() 
    {
        emit result("workers are the best");
    }

signals:
    void result(QString data);
};

// Somewhere in MainWindow
QThread* thread = new Thread(this);
Worker* worker = new Worker();
worker->moveToThread(thread);

connect(thread, &QThread::finished, 
        worker, &QObject::deleteLater);
connect(this, &MainWindow::startWork, 
        worker, &Worker::doWork);
connect(worker, &Worker::resultReady, 
        this, handleResult);

thread->start();

// later on, to stop the thread
thread->quit();
thread->wait();

We start by creating a Worker class that has the following:

  • A doWork()slot that will have the content of our old QThread::run() function
  • A result()signal that will emit the resulting data

Next, in MainWindow, we create a simple thread and an instance of Worker. The worker->moveToThread(thread) function is where the magic happens. It changes the affinity of the worker object. The worker now lives in the thread object.

You can only push an object from your current thread to another thread. Conversely, you cannot pull an object that lives in another thread. You cannot change the thread affinity of an object if the object does not live in your thread. Once thread->start() is executed, we cannot call worker->moveToThread(this) unless we are doing it from this new thread.

After that, we will use three connect() functions:

  • We handle worker life cycle by reaping it when the thread is finished. This signal will use a Qt::DirectConnection function.
  • We start the Worker::doWork() upon a possible UI event. This signal will use a Qt::QueuedConnection.
  • We process the resulting data in the UI thread with handleResult(). This signal will use a Qt::QueuedConnection.

To sum up, QThread can be either subclassed or used in conjunction with a worker class. Generally, the worker approach is favored because it separates more cleanly the threading affinity plumbing from the actual operation you want to execute in parallel.

Flying over Qt multithreading technologies

Built upon QThread, several threading technologies are available in Qt. First, to synchronize threads, the usual approach is to use a mutual exclusion (mutex) for a given resource. Qt provides it by the mean of the QMutex class. Its usage is straightforward:

QMutex mutex;
int number = 1;

mutex.lock();
number *= 2;
mutex.unlock();

From the mutex.lock() instruction, any other thread trying to lock the mutex object will wait until mutex.unlock() has been called.

The locking/unlocking mechanism is error prone in complex code. You can easily forget to unlock a mutex in a specific exit condition, causing a deadlock. To simplify this situation, Qt provides a QMutexLocker that should be used where the QMutex needs to be locked:

QMutex mutex;
QMutexLocker locker(&mutex);

int number = 1;
number *= 2;
if (overlyComplicatedCondition) {
    return;
} else if (notSoSimple) {
    return;
}

The mutex is locked when the locker object is created, and it will be unlocked when locker is destroyed, for example, when it goes out of scope. This is the case for every condition we stated where the return statement appears. It makes the code simpler and more readable.

If you need to create and destroy threads frequently, managing QThread instances by hand can become cumbersome. For this, you can use the QThreadPool class, which manages a pool of reusable QThreads.

To execute code within threads managed by a QThreadPool, you will use a pattern very close to the worker we covered earlier. The main difference is that the processing class has to extend the QRunnable class. Here is how it looks:

class Job : public QRunnable
{
    void run()
    {
        // long running operation
    }
}

Job* job = new Job();
QThreadPool::globalInstance()->start(job);

Just override the run() function and ask QThreadPool to execute your job in a separate thread. The QThreadPool::globalInstance() function is a static helper function that gives you access to an application global instance. You can create your own QThreadPool class if you need to have a finer control over the QThreadPool life cycle.

Note that QThreadPool::start() takes the ownership of the job object and will automatically delete it when run() finishes. Watch out, this does not change the thread affinity like QObject::moveToThread() does with workers! A QRunnable class cannot be reused, it has to be a freshly baked instance.

If you fire up several jobs, QThreadPool automatically allocates the ideal number of threads based on the core count of your CPU. The maximum number of threads that the QThreadPool class can start can be retrieved with QThreadPool::maxThreadCount().

If you need to manage threads by hand, but you want to base it on the number of cores of your CPU, you can use the handy static function, QThreadPool::idealThreadCount().

Another approach to multithreaded development is available with the Qt Concurrent framework. It is a higher level API that avoids the use of mutexes/locks/wait conditions and promotes the distribution of the processing among CPU cores.

Qt Concurrent relies of the QFuture class to execute a function and expect a result later on:

void longRunningFunction();
QFuture<void> future = QtConcurrent::run(longRunningFunction);

The longRunningFunction() will be executed in a separated thread obtained from the default QThreadPool class.

To pass parameters to a QFuture class and retrieve the result of the operation, use the following code:

QImage processGrayscale(QImage& image);
QImage lenna;

QFuture<QImage> future = QtConcurrent::run(processGrayscale,
    lenna);
QImage grayscaleLenna = future.result();

Here, we pass lenna as a parameter to the processGrayscale() function. Because we want a QImage as a result, we declare QFuture with the template type QImage. After that, future.result() blocks the current thread and waits for the operation to be completed to return the final QImage template type.

To avoid blocking, QFutureWatcher comes to the rescue:

QFutureWatcher<QImage> watcher;
connect(&watcher, &QFutureWatcher::finished, 
        this, &QObject::handleGrayscale);

QImage processGrayscale(QImage& image);
QImage lenna;
QFuture<QImage> future = QtConcurrent::run(processImage, lenna);
watcher.setFuture(future);

We start by declaring a QFutureWatcher with the template argument matching the one used for QFuture. Then, simply connect the QFutureWatcher::finished signal to the slot you want to be called when the operation has been completed.

The last step is to the tell the watcher to watch the future object with watcher.setFuture(future). This statement looks almost like it’s coming from a science fiction movie.

Qt Concurrent also provides a MapReduce and FilterReduce implementation. MapReduce is a programming model that basically does two things:

  • Map or distribute the processing of datasets among multiple cores of the CPU
  • Reduce or aggregate the results to provide it to the caller

check styleThis technique has been first promoted by Google to be able to process huge datasets within a cluster of CPU.

Here is an example of a simple Map operation:

QList images = ...;

QImage processGrayscale(QImage& image);
QFuture<void> future = QtConcurrent::mapped(
                                     images, processGrayscale);

Instead of QtConcurrent::run(), we use the mapped function that takes a list and the function to apply to each element in a different thread each time. The images list is modified in place, so there is no need to declare QFuture with a template type.

The operation can be made a blocking operation using QtConcurrent::blockingMapped() instead of QtConcurrent::mapped().

Finally, a MapReduce operation looks like this:

QList images = ...;

QImage processGrayscale(QImage& image);
void combineImage(QImage& finalImage, const QImage& inputImage);

QFuture<void> future = QtConcurrent::mappedReduced(
                                            images, 
                                            processGrayscale, 
                                            combineImage);

Here, we added a combineImage() that will be called for each result returned by the map function, processGrayscale(). It will merge the intermediate data, inputImage, into the finalImage. This function is called only once at a time per thread, so there is no need to use a mutex object to lock the result variable.

The FilterReduce reduce follows exactly the same pattern, the filter function simply allows to filter the input list instead of transforming it.

Summary

In this article, we discovered how a QThread works and you learned how to efficiently use tools provided by Qt to create a powerful multi-threaded application.

Resources for Article:


Further resources on this subject:


1 COMMENT

  1. Do not want to be too picky, but in QFutureWatcher example shouldn’t the line
    QFuture future = QtConcurrent::run(processImage, lenna);
    be
    QFuture future = QtConcurrent::run(processGrayscale, lenna);

LEAVE A REPLY

Please enter your comment!
Please enter your name here