Asynchronous Programming – Part 3: Concurrency in C++ with Qt6
Qt6 is a widely used commercial framework for C++ with its own mechanisms for asynchronous control flow.
(Image: Scott Prokop / Shutterstock.com)
- Martin Meeser
The Qt6 framework is widely used, particularly for its powerful tools for creating user interfaces for embedded devices with C++. However, its applications extend beyond embedded systems.
This article is the third part of the series “Asynchronous Programming,” which introduces mechanisms and frameworks for the efficient and sustainable development of non-linear program flows in various languages. Asynchronous programs do not use error-prone and difficult-to-debug thread mechanisms like locks and semaphores; consequently, they do not waste resources by blocking threads, and they scale natively with the available processor cores.
In contrast to the previously introduced mechanisms of the Boost::Asio library, which are de facto the C++ standard and therefore embedded as natively as possible in C++, Qt, from its history, offers an event system: the Qt-native signal-slot mechanism, QThreadPool, and – more recently – QFuture and QPromise.
On current Debian and Ubuntu Linux systems, Qt6 can be installed via the familiar APT package manager:
sudo apt install qt6-base-dev
This currently installs version 6.8.2. If you want to use the latest version (currently 6.10.2) or are working with a different system, you will need a user account with the Qt Group, the company that develops Qt, for the download. Qt is divided into various modules. The examples in this article require QtCore, which is necessary for every Qt project and provides basic functions, as well as QtConcurrent for concurrency.
Programs can be built using the manufacturer's qmake tool or Qt6's CMake package. The following listing shows a typical configuration in CMakeLists.txt for a Qt6 project without a user interface.
cmake_minimum_required(VERSION 3.18)
project(MyQt6ConsoleApp VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(Qt6 REQUIRED COMPONENTS Core Concurrent)
qt_standard_project_setup()
qt_add_executable(MyQt6ConsoleApp
src/main.cpp
)
target_link_libraries(MyQt6ConsoleApp PRIVATE Qt6::Core Qt6::Concurrent)
install(TARGETS MyQt6ConsoleApp
RUNTIME DESTINATION bin
Listing 1: A typical example for CMakeLists.txt for a console application with Qt6
Events and Qt's Event Loop
The main function of every Qt application follows the same pattern: Developers create an instance of the main class, here QCoreApplication, and before exiting the function, call the exec() method of this instance to start Qt's event loop.
Event loops provide the mechanisms and API for asynchronous development and orchestrate the program flow. Qt's event loop also receives events from the operating system and forwards them. The basic structure of a Qt6 application, including starting the event loop, looks like this:
#include <QCoreApplication>
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
// ...
return app.exec();
Listing 2: Basic structure of a Qt6 application that starts a Qt event loop.
The exec() method only returns when QCoreApplication::quit() is called. In this simple example, the program will therefore never end on its own; the application's main thread is used for the event loop. Graphical applications use one of the derivatives of this class instead of QCoreApplication: QAndroidService or QGuiApplication. To react to events, the corresponding class must inherit from QObject and override the event() or customEvent() method. All events are derived from the QEvent class. There are a number of defined event classes in the Qt Framework, for example QEvent:Timer, QEvent:SockAct, and QEvent:Quit. Developers can also create their own event types by creating a class derived from QEvent.
With the static method postEvent() of the QCoreApplication class, specific events are passed to the event loop, as the following listing shows.
#include <QCoreApplication>
const QEvent::Type MyEventType = QEvent::Type(QEvent::User + 1);
class MyEvent : public QEvent
{
public:
MyEvent(const QString& msg) : QEvent(MyEventType), message(msg) {}
QString message;
};
class MyReceiver : public QObject
{
Q_OBJECT
protected:
void customEvent(QEvent* ev) override
{
if (ev->type() == MyEventType)
{
MyEvent* myEvent = static_cast<MyEvent*>(ev);
qDebug() << myEvent->message;
QCoreApplication::quit();
}
}
};
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
MyReceiver receiver;
QCoreApplication::postEvent(&receiver, new MyEvent("Kind of async
hello world!"));
return app.exec();
Listing 3: Examples for asynchronous execution with a custom event type.
Videos by heise
The postEvent() method queues the event and returns immediately. However, so far there is only one thread: the main thread. This means that the program blocks as soon as code that itself blocks runs within event processing. Even a long-running event delays the entire program flow, leading to a program freeze that is perceptible to users.
Therefore, it has proven effective in Qt to explicitly create one or more worker threads. To achieve this, a new instance of the QThread class is created. Calling the QThread::start() method executes a separate event loop within the QThread.
In Qt, the principle of thread affinity applies: Every QObject is associated with a QThread, initially the one in which it was created. Event processing always occurs in the thread associated with the QObject. However, the thread can be changed using the QObject::moveToThread() method.
In this way, tasks run in parallel, as the following listing shows as an example.
// MyReceiver und MyEvent analog zu Listing 3
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
qDebug() << QThread::currentThreadId();
MyReceiver receiver;
QThread qThread;
qThread.start();
receiver.moveToThread(&qThread);
QCoreApplication::postEvent(&receiver, new MyEvent(QString("Kind of async hello world from 2nd thread")));
return app.exec();
}
Listing 4: Creating and using a QThread with its event loop.
The postEvent() method returns immediately, and the code does not use complicated and error-prone mechanisms like locks or semaphores; however, the creation and assignment of individual threads are explicit. This example therefore does not scale automatically with the available resources, as is the case with a thread pool. The following figure summarizes the basic classes of a Qt application in a UML class diagram.