There are three ways to register Qt types for multithreading, namely inheriting QThread, using QObject’s moveToThread function and Qtconcurrent concurrent threads.

In many articles, it is recommended to inherit from the QThread class and override the run method to use time-consuming manipulation code in run. This approach makes us think that QThread is the entity of the thread. Creating a QThread object is thought of as opening a new thread. This tricky approach seems to help us get started quickly, but a deeper understanding of multithreaded programming shows that it takes the code out of our control and makes it more and more complex to write. The most typical problem is that the code is put into a new thread, but still runs in the old thread.

Where should we put the time-consuming code?

Leaving multithreading out of the equation for now, we generally encapsulate time-consuming code in a class. In the case of considering multi-threading, do we have to strip out the code and put it somewhere? Actually, we don’t have to do that. In qt4, we needed to use methods that inherited from QThread, which would break our original code structure, and the run method could only run one piece of code, and we couldn’t encapsulate so many QThreads if we had thousands of functions.

So in Qt5, the Qt library improved the thread affinity and signal slot mechanism, and we have a more elegant way to use threads, namely QObject::moveToThread(). This is also the official recommended practice.

Let’s prepare two classes to introduce and explain the workflow.

controller.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QObject>
#include <QDebug>
class Controller : public QObject {
    Q_OBJECT
public:
    explicit Controller(QObject *parent = nullptr)
        : QObject(parent) {}
signals:
    void requestPing();
public slots:
    void pong() {
        qDebug() << Q_FUNC_INFO << this->thread();
        qDebug() << "pong";
    }
};
#endif // CONTROLLER_H

handler.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#ifndef HANDLEER_H
#define HANDLEER_H
#include <QObject>
#include <QDebug>
#include <QThread>
class Handler : public QObject {
    Q_OBJECT
public:
    explicit Handler(QObject *parent = nullptr)
        : QObject(parent) {}
signals:
    void requestPong();
public slots:
    void ping() {
        qDebug() << Q_FUNC_INFO << this->thread();
        emit requestPong();
    }
};
#endif // HANDLEER_H

Initialize the object in main.cpp and connect the signals and slots.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <QCoreApplication>
#include <QThread>
#include "controller.h"
#include "handleer.h"
int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);
    QThread* handleThread = new QThread;
    Controller* controller = new Controller;
    Handler* handler = new Handler;
    // 移动对象到新的线程
    handler->moveToThread(handleThread);
    handleThread->start();
    // 将对象的信号的槽绑定,注意必须在 moveToThread 之后链接。
    QObject::connect(controller, &Controller::requestPing, handler, &Handler::ping);
    QObject::connect(handler, &Handler::requestPong, controller, &Controller::pong);
    emit controller->requestPing();
    return a.exec();
}

Take a look at the execution results:

1
2
3
void Handler::ping() QThread(0x14ee080)
void Controller::pong() QThread(0x14e9e60)
pong

You can see that the QThread object obtained by the two functions is not the same anymore. Use movetothread to move an object to a new thread and call the target function with a signal for the purpose of executing in the new thread.

In this way, we can easily control the execution of the thread by manipulating the QThread object, such as setting the thread’s priority level, suspending it or resuming it. Moreover, this approach can be more intuitive than inheriting QThread, which is just a thread management class and not a thread entity. If we use the inheritance approach, we will think that QThread is the thread entity, which will cause some cognitive confusion.

QtConCurrent can automatically adjust the number of threads running according to the number of CPU cores in the computer.

Before using QtConcurrent you need to add the corresponding Qt module concurrent.

When using it, we need to add a QFutureWatcher object to control and execute a QFuture object, and receive the result of the execution of the QFuture object via the finished signal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
QFutureWatcher<bool>* watcher = new QFutureWatcher<bool>();
QObject::connect(watcher, &QFutureWatcher<bool>::finished, watcher, [=] {
    const bool result = watcher->result();
    qDebug() << result;
    watcher->deleteLater();
});
QFuture<bool> future = QtConcurrent::run([=]() -> bool {
    return true;
});
watcher->setFuture(future);

The above is a simple example, and it’s easy to see that Qt provides us with a pretty good solution. This form is more similar to the way QDbus objects use QDbusPendingCallWatcher to get results asynchronously, and it’s very easy to get started with. When using multithreading, we need to pay attention to a few things: mutual exclusion and synchronous synchronization, type registration, and opening threads in threads.

In multi-threaded development, we need to pay attention to a little bit more, the most important is thread synchronization, we need to use some means so that the data that functions in different threads can access correctly.

  • Mutually exclusive: A public resource can only be used by one process or thread at the same moment, and multiple processes or threads cannot use the public resource at the same time.
  • Synchronization: Two or more processes or threads synchronize their steps during operation and run in a predefined order.

Solutions: Mutual exclusion locks, conditional variables, read/write locks, spin locks, semaphores (mutual exclusion and synchronization).

In Qt programming, we can use Qt’s signal and slot mechanism to achieve communication between two objects, whether they are in the same thread or not, but we need to register the parameters to Qt’s meta-object system to pass them, otherwise Qt will not be able to complete the data transfer.

There are two ways to register custom types in Qt, one is the qRegisterMetaType() function and the Q_DECLARE_METATYPE(Type) macro.

These two registration methods have different roles. Using the qRegisterMetaType() function allows custom types to be passed in Qt’s signal slots, while the Q_DECLARE_METATYPE(Type) macro allows registered custom types to be wrapped using QVariant.

We also need to be aware in multi-threaded development that there is semi-automatic memory management in Qt, and this memory management approach affects how we use multi-threaded development. When we create a new QObject object, if we make a parent, the object will be thread-bound to the parent object. If the two objects are in different threads, Qt will warn us that the parent object’s thread and the current object’s thread are not the same, and they will not be able to use Qt’s connect function for message passing.