Asynchrone Programmierung – Teil 3: Parallelität in C++ mit Qt6
Qt6 ist ein weit verbreitetes kommerzielles Framework für C++ mit eigenständigen Mechanismen zur asynchronen Ablaufsteuerung.
(Bild: Scott Prokop / Shutterstock.com)
- Martin Meeser
Das Qt6-Framework ist insbesondere wegen seiner mächtigen Werkzeuge zur Erstellung von Bedienoberflächen für eingebettete Geräte mit C++ weit verbreitet. Doch die Einsatzzwecke gehen über Embedded hinaus.
Dieser Artikel ist der dritte Teil aus der Serie „Asynchrone Programmierung“, die Mechanismen und Frameworks für die effiziente und nachhaltige Entwicklung von nichtlinearen Programmabläufen in verschiedenen Sprachen vorstellt. Asynchrone Programme benutzen keine fehleranfälligen und schwer zu debuggenden Thread-Mechanismen wie Locks und Semaphoren, sie verschwenden folglich keine Ressourcen durch Blockieren von Threads und sie skalieren nativ mit den zur Verfügung stehenden Prozessorkernen.
Im Gegensatz zu den bereits vorgestellten Mechanismen der Boost::Asio-Bibliothek, die de facto den C++ Standard darstellen und daher möglichst nativ in C++ eingebettet sind, bietet Qt aus seiner Historie heraus ein Event-System: den Qt-eigenen Signal-Slot-Mechanismus, QThreadPool und – etwas neuer – QFuture und QPromise.
Auf aktuellen Debian- und Ubuntu-Linuxen kann man Qt6 über die bekannte Paketverwaltung APT installieren:
sudo apt install qt6-base-dev
Dies installiert derzeit die Version 6.8.2. Möchte man die neueste Version (aktuell 6.10.2) verwenden oder arbeitet man mit einem anderen System, benötigt man für den Download einen Benutzeraccount bei der Qt Group, dem Unternehmen, das Qt entwickelt. Qt ist in verschiedene Module aufgeteilt. Die Beispiele in diesem Artikel benötigen QtCore, das in jedem Qt-Projekt erforderlich ist und Grundfunktionen liefert, sowie QtConcurrent für Nebenläufigkeit.
Die Programme lassen sich mit dem Tool qmake des Herstellers oder mit dem CMake-Paket von Qt6 bauen. Das folgende Listing zeigt eine typische Konfiguration in CMakeLists.txt für ein Qt6-Projekt ohne Bedienoberfläche.
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: Ein typisches Beispiel für CMakeLists.txt für eine Konsolenanwendung mit Qt6
Events und die Event Loop von Qt
Die Hauptfunktion jeder Qt-Anwendung folgt dem gleichen Muster: Entwicklerinnen und Entwickler erzeugen eine Instanz der Hauptklasse, hier QCoreApplication, und rufen vor dem Beenden der Funktion die Methode exec() dieser Instanz auf, um die Event Loop von Qt zu starten.
Event Loops stellen die Mechanismen und die API für die asynchrone Entwicklung bereit und orchestrieren den Programmablauf. Die Event Loop von Qt nimmt auch Ereignisse vom Betriebssystem entgegen und leitet diese weiter. Das Grundgerüst einer Qt6-Anwendung mit Starten der Event Loop sieht folgendermaßen aus:
#include <QCoreApplication>
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
// ...
return app.exec();
Listing 2: Grundgerüst einer Qt6-Anwendung, die eine Qt Event Loop startet.
Die Methode exec() springt erst beim Aufruf von QCoreApplication::quit() zurück. In diesem einfachen Beispiel wird das Programm also niemals von sich aus enden, der Hauptthread der Applikation wird für die Event Loop verwendet. Grafische Anwendungen verwenden statt QCoreApplication eine der Ableitungen dieser Klasse: QAndroidService oder QGuiApplication. Um auf Events reagieren zu können, muss die entsprechende Klasse von der Klasse QObject erben und die Methode event() oder customEvent() überschreiben. Alle Events stammen von der Klasse QEvent ab. Es gibt im Qt Framework eine Reihe von definierten Event-Klassen, zum Beispiel QEvent:Timer, QEvent:SockAct und QEvent:Quit. Entwickler können auch eigene Event-Typen erzeugen, indem sie eine von QEvent abgeleitete Klasse anlegen.
Mit der statischen Methode postEvent() der Klasse QCoreApplication werden konkrete Events an die Event Loop übergeben, wie folgendes Listing zeigt.
#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: Beispiele für asynchrone Ausführung mit einem eigenen Event-Typen.
Videos by heise
Die Methode postEvent() reiht das Ereignis in die Warteschlange ein und springt sofort zurück. Bisher gibt es allerdings nur einen Thread: den Haupt-Thread. Das bedeutet, dass das Programm blockiert, sobald innerhalb der Ereignisverarbeitung Code läuft, der selbst blockiert. Auch ein lang andauerndes Event verzögert den gesamten Programmablauf, was zu einem für Anwenderinnen und Anwender wahrnehmbaren Einfrieren des Programms führt.
Daher hat sich in Qt bewährt, explizit einen oder mehrere Arbeiter-Threads („Worker Threads“) zu erzeugen. Dazu legt man eine neue Instanz der Klasse QThread an. Ein Aufruf der Methode QThread::start() führt innerhalb des QThread eine separate Event Loop aus.
In Qt gilt der Grundsatz der Thread-Zugehörigkeit („Thread Affinity“): Jedes QObject ist einem QThread zugeordnet, initial demjenigen, in dem es erstellt wurde. Die Event-Verarbeitung erfolgt immer in dem Thread, der dem QObject zugeordnet ist. Der Thread lässt sich mit der Methode QObject::moveToThread() jedoch wechseln.
Auf diese Weise verlaufen Aufgaben parallel, wie folgendes Listing exemplarisch zeigt.
// 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: Erstellen und Verwenden eines QThread mit eigener Event-Loop.
Die Methode postEvent() springt sofort zurück und der Code setzt keine komplizierten und fehleranfälligen Mechanismen wie Locks oder Semaphore ein, die Erstellung und Zuordnung einzelner Threads sind aber explizit. Dieses Beispiel skaliert also nicht automatisch mit den zur Verfügung stehenden Ressourcen, so wie es bei einem Threadpool der Fall ist. Die folgende Abbildung fasst die grundlegenden Klassen einer Qt-Anwendung in einem UML-Klassendiagramm zusammen.