DEV Community

Cover image for [Qt] VII. Multi-threads Signals and Slots
Misinahaiya
Misinahaiya

Posted on

[Qt] VII. Multi-threads Signals and Slots

"Multitasking is a part of my everyday life."

Monica Denise Brown

I could feel your brain's absolute pain of binge-eating those information last article. To simplify things, before getting into the topic, I provide some useful tips in using signals:

  1. A signal can connect to a slot;
  2. A slot can receive more than one signal;
  3. A signal can connect to more than one slot;
  4. Number and types of parameters in signals and slots must match, otherwise errors occur;
  5. The number of parameters of the slot could be less than the number of the signal's, but not more than.
  6. The connection between signals and slots is dynamic, i.e. executed in runtime.
  7. Signals could connect to another signal, but slots could not connect to another slot.

For the reason why, you could refer to my previous article and use a bit of your logic.

Prerequisites

For complete novices at threading and concurrency (sometimes multitasking/ multi-processing), you could just consider the smallest unit of threading - threads - as different processes running at the same time.

In Qt, there are many thread-safe functions and methods. The term thread-safety means that the variables and members always maintain a valid state, i.e. they're not interfered.

A situation where thread-safety does not exist:

  • There's an integer variable, say, a, initialized as 20.
  • Thread 1 changes it to 30.
  • Thread 2 uses it to calculate something, and process the result.

This looks all fine. But what if, say:

  • ...
  • Thread 1 changes it to 30.
  • Thread 3 accidentally changes it to 0.
  • Thread 2 uses it to calculate something, which is expected as 30 but got 0.

If Thread 2 is processing an internal event or process, that is still fine; at most a segmentation fault or zero-division error will occur and the application will crash. However, if Thread 2 is handling, say, OS-wide event or system-related or critical processes, then your computer will screw up. Say, if you're using your computer for an important reason, your whole day will be bad, I guess.

From here, you could see how important thread safety is.

Connection Types

I've introduced the basic signal connection syntax:

connect(signalObject, signal, slotObject, slot);
Enter fullscreen mode Exit fullscreen mode

Actually, there's a hidden Qt::ConnectionType connectionType behind. It defaults to Qt::ConnectionType::AutoConnection.

Here's the enumerators:

  • Qt::ConnectionType::AutoConnection: The default one of the QObject::connect (as just mentioned). If the signal and slot are on the same thread (you'll get to know what does it mean), it becomes Qt::ConnectionType::DirectConnection. Else, it becomes Qt::ConnectionType::QueuedConnection.
  • Qt::ConnectionType::DirectConnection: As the name suggests, when the signal emits, the slot will be called instantly. This is not thread-safe (as mentioned).
  • Qt::ConnectionType::QueuedConnection: The slot will not be called instantly after the signal emits. Instead, a private class (i.e. an internal class used by Qt itself only, you could not normally access it) called QMetaCallEvent is sent to the receiver. It is an event (wait for and see the later articles). The slot is executed in the receiver's thread.
  • Qt::ConnectionType::BlockingQueuedConnection: Same as Qt::ConnectionType::QueuedConnection, but the signalling thread blocks until the slot returns. You should not use this one in a single-threaded application, or else your application will become unresponsive (But who on earth will modify these settings manually!?)

For queued connections, the thread is actually called by QApplication::postEvent(). This explains that why it usually calls the slot inside the thread that the object of the slot currently lives in.

Also, for queued connections, I suggest you explicitly pointing out to the compiler that you're using queued connections, as it makes the code more readable and reduces error occurrences (well, probably).

There are two flags added so that you could use the a bitwise OR operator (|) to combine them with the above connection types.

  • Qt::ConnectionType::UniqueConnection: Ensures the uniqueness of the connection, i.e. QObject::connect will fail if the same signal has already been connected to the same slot of the same pair of objects.
  • Qt::ConnectionType::SingleShotConnection: Ensures the singleness of the emission, i.e. emit-ing the signal will not work if the signal has already been emitted once. That is, the connection will be automatically broken after the signal is emitted.

Do remember that you could actually use QObject::sender to access the sender of the signal; yet this violates the design patterns of OOP (object-oriented programming). In addition, there are few points to know in using sender():

  • Its return type is QObject; you need to use qobject_cast to convert it to the desired type (wait and see the article introducing the dynamic system of Qt).
  • An invalid pointer is returned when your function/ method scope is not a signal (well, fair enough. How on earth could you ask your fish how many Spanish words he or she knows?)
  • An invalid pointer is returned when your slot does not live in the same thread as the sender (well, still fair enough. How could you possibly send a message to some random aliens that live 8000 light years away? By telepathy?)

Well, it's wetter than the theories and explanations of the previous article, but it's still very dry. So, I'll craft yet another mini-example showcasing the mighty trans-thread communication system of the signal-slot mechanism of the meta-object system of the module <QtCore> of Qt!

Little Example

There's a little, simple and intuitive app, say - the corresponding project is titled as "Countdown10Seconds" - which counts down 10 seconds commencing from the start of the app (to be precise, it is the time when the thread receives the start-counting signal; and to be even more precise, the term "signal" I have just used has nothing to do with the "signal" as in signal-slot connections). There is a label showing you the thread information, remaining seconds, etc. After the timeout, there will be a newline appended to the label's text, which read "10 seconds have passed."

We have to create 2 classes (i.e. 4 files using the language spoken by Qt Creator), SecondObject and CountThread.

Without further ado, as my usual practice, I will show you my code and explain as detailed as possible.

  • secondobject.h:
#ifndef SECONDOBJECT_H
#define SECONDOBJECT_H

#include <QObject>

class SecondObject : public QObject
{
    Q_OBJECT

public:
    explicit SecondObject(int seconds, QObject *parent = nullptr);

signals:
    void secondPassed(int seconds, unsigned long long identifier);

public slots:
    void onTimeout();

private:
    int m_seconds;
};

#endif // SECONDOBJECT_H
Enter fullscreen mode Exit fullscreen mode
  • secondobject.cpp:
#include "secondobject.h"

#include <QThread>


SecondObject::SecondObject(int seconds, QObject *parent)
    : QObject(parent),
    m_seconds(seconds)
{}

void SecondObject::onTimeout()
{
    if (m_seconds >= 0)
    {
        unsigned long long identifier = (unsigned long long)QThread::currentThreadId();
        emit secondPassed(m_seconds, identifier);
        --m_seconds;
    }

    else
    {
        QThread::currentThread()->exit(0);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • countthread.h:
#ifndef COUNTTHREAD_H
#define COUNTTHREAD_H

#include <QThread>
#include <QPointer>

class MainWindow;

class CountThread : public QThread
{
    Q_OBJECT

public:
    CountThread(MainWindow *parent);
    ~CountThread(){}

protected:
    void run() override;

private:
    QPointer<MainWindow> m_parent;
};

#endif // COUNTTHREAD_H
Enter fullscreen mode Exit fullscreen mode
  • countthread.cpp:
#include "countthread.h"
#include "secondobject.h"
#include "mainwindow.h"

#include <QTimer>


CountThread::CountThread(MainWindow *parent)
    : m_parent(parent)
{}

void CountThread::run()
{
    QTimer timer;
    SecondObject secondObject(10); // 10 seconds
    connect(&timer,
            &QTimer::timeout,
            &secondObject,
            &SecondObject::onTimeout);
    timer.start(1000); // 1000 milliseconds, discussed already earlier

    if (!m_parent.isNull())
        connect(&secondObject,
                &SecondObject::secondPassed,
                m_parent.data(),
                &MainWindow::onSecondLeft);

    exec();
}
Enter fullscreen mode Exit fullscreen mode
  • mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QLabel>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

public slots:
    void onSecondLeft(int seconds, unsigned long long identifier);

private:
    QLabel *m_label;
};
#endif // MAINWINDOW_H
Enter fullscreen mode Exit fullscreen mode
  • mainwindow.cpp:
#include "mainwindow.h"
#include "countthread.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    m_label = new QLabel(this);
    m_label->setAlignment(Qt::AlignmentFlag::AlignCenter);
    m_label->setMargin(25);
    setCentralWidget(m_label);
    setWindowTitle("Misinahaiya - Multi-thread Signals");

    CountThread *thread = new CountThread(this);
    connect(thread,
            &CountThread::finished,
            thread,
            &CountThread::deleteLater);
    thread->start();
}

MainWindow::~MainWindow()
{}

void MainWindow::onSecondLeft(int seconds, unsigned long long identifier)
{
    QString string = QString("There is/are %1 second(s) left, and the current (executing) thread identifier is %2.\n"
                             "The second-counting thread's identifier is %3.")
            .arg(seconds).arg((unsigned long long)QThread::currentThreadId()).arg(identifier);
    if (seconds == 0)
        string.append("\n10 seconds have passed.");
    m_label->setText(string);
}
Enter fullscreen mode Exit fullscreen mode
  • main.cpp:
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
Enter fullscreen mode Exit fullscreen mode

Results

Here are the two tiny images showcasing the results of our tiny ~~ and useless~~ application:

  • Not yet been timeout:
    Not yet been timeout

  • Timeout already:

Timeout already

Explanations

Analyzing the headers, we could see that the SecondObject::secondPassed signal will be emitted every second.

The counting thread inherits QThread as you could see, and QThread is the most basic threading class in Qt (somehow canonical too, you will be introduced later after you have mastered the use of Qt GUI and Qt Widgets).

To make use of the QThread::start method and send signals efficiently, I overrode the virtual method QThread::run. As the candle wick (the initiator, sorry for the bewilderment if you do not understand my metaphor) of a QThread is typically QThread::start, and it calls its underlying protected method QThread::run, overriding it is a good way to prevent reinventing the wheel.

As the thread is considered useless after it exited in this case (i.e. the program guarantees that the thread will not be reused), so it's safe to delete this on finishing calling the method exit(0) (where "0" symbolizes no errors occurred, same as the Unix return codes). However, to really ensure no one is using the thread anymore, I have introduced a new method QObject::deleteLater to you. It schedules the removal of the unwanted object, i.e. the mighty Qt (meta-) object system has an intricate and meticulous way to ensure nothing is using the object anymore before the removal. If you're interested, please go and look at some docs that are better than mine.

On introducing the QObject::deleteLater slot, I think I also need to introduce a completely new class to you QPointer. Have you ever used smart pointers? If yes, then it's pretty simple and similar in nature. If you haven't (oh my, are you a C programmer from the 90's?), then I'll explain it in few sentences in simple English: a smart pointer is a pointer that automatically detects the deletion of the linked object. As a pointer is a reference that is pointing to an object by definition, it's just a bunch of binary numbers. So, if you have a pointer pointing to 0 (no object) or nullptr, it's a null pointer; to a deleted object, it's a dangling pointer; to an invalid object, it's a raw pointer. You do not need to memorize these terms, as it is not the main point. You just need to know that there's a magical class in Qt, QPointer, where it overrides the operators . and -> so that you could access meta-pointer information (like if it is deleted, null etc.) and the pointer itself.

So, the above example is a practical usage of not only QThread and QObject::connect, but also QPointer!

One last thing is QString. It's a Qt string class. We will discuss this more deeply in the following topics, covering the basic types of Qt like containers (lists, arrays, maps, etc.) and strings (chars, strings, byte arrays, etc.)

The entry function main is generated by Qt Creator and it's very typical. So I'll not dive deep into it as I've already explained it.

For convenience and conciseness, Qt::ConnectionType::QueuedConnection is not used. However, for actual practice, I do suggest you adding it explicitly, but I wouldn't urge you to do so as, really, it does not matter, for the QObject::connect method will automatically detect so.

Conclusion

Right, so this chapter is considered as a little addendum to the previous ultra-long article. I know that explaining and putting bunch of examples regarding the feature-rich signal-slot system in one fell swoop would make you faint, so I'll split it into the single-threaded worked-examples (the previous one) and the multi-threaded worked-example (this one).

Anyway, hopefully you got what signals really do. At least you could (almost) master the signal-slot system in Qt, and it's about 70% in terms of the weighing of the importance of the Qt meta-object system.

In the next article or two, I'll be introducing the rest of the meta-object system features. Still that line, stayed tuned to this series and practice your Qt skill constantly!

Top comments (0)