Parallelprogrammierung mit C++ und Qt, Teil 2: Bildsequenzen parallel berechnen

Seit mehr als einem Jahrzehnt bietet Qt in gewohnt komfortabler und plattformunabhängiger Weise Zugriff auf Funktionen und Konstrukte für die Parallelprogrammierung. Der zweite Teil des Tutorials stellt die grundlegenden Klassen in einfachen Beispielen vor.

In Pocket speichern vorlesen Druckansicht 18 Kommentare lesen
Lesezeit: 16 Min.
Von
  • Matthias Nagorni
Inhaltsverzeichnis

Seit mehr als einem Jahrzehnt bietet Qt in gewohnt komfortabler und plattformunabhängiger Weise Zugriff auf Funktionen und Konstrukte für die Parallelprogrammierung. Der zweite Teil des Tutorials stellt die grundlegenden Klassen in einfachen Beispielen vor.

Für die Parallelisierung von Programmcode gibt es im Wesentlichen zwei Motive, die häufig auch in Kombination auftreten: erstens die Steigerung der Performance durch Ausnutzung paralleler Hardware und zweitens eine Verbesserung der Latenz durch Trennung zeitkritischer und langsamer Prozesse. Nachdem in den letzten Jahren Multi-Core-Prozessoren zum Standard geworden sind, wird leistungshungrige Software in Zukunft nur dann vollständig von den Errungenschaften neuer Prozessorgenerationen profitieren können, wenn ihre rechenintensiven Prozesse so weit wie möglich parallelisiert sind. Demnach ist es höchste Zeit, sich mit Parallelprogrammierung vertraut zu machen.

Mehr Infos

Plattformunabhängige Parallelprogrammierung mit C++ und Qt

Nokias Qt Framework implementiert zunächst Klassen für den Zugriff auf die grundlegenden Elemente der Parallelprogrammierung, zum Beispiel Thread, Mutex und Condition-Variablen. Mit der 2008 veröffentlichten Version 4.4 wurde die API für Multithreading deutlich erweitert, und so findet man inzwischen Features, die vom Thread Pool bis zur Implementierung eines MapReduce-Frameworks reichen. Qt eignet sich daher sowohl für den Einstieg in die Parallelprogrammierung als auch die Realisierung komplexer paralleler Applikationen.

Mehr Infos

Quellcode zum Artikel

  • QParallelDemo1
  • QParallelDemo2
  • QParallelDemo3
  • QParallelJulia-1.0.5
  • QParallelMandelbrot-1.0.6
  • QJulia-1.0.2

Zu finden hier.

[Update] Die Beispiele wurden gegenüber der ursprünglichen Version noch einmal leicht überarbeitet. In main.cpp werden QApplication und QMainWindow nicht mehr dynamisch erzeugt, wodurch ihr Destruktor bei Programmende automatisch aufgerufen wird. Damit sich nun die Kette der Destruktoren in QParallelMandelbrot, QParallelJulia und QJulia sauber abarbeiten lässt, sind sämtliche Threads im Destruktor der Klasse Gui zunächst per wait() mit dem Haupt-Thread zu synchronisieren. Um längere Wartezeiten zu vermeiden, signalisiert dabei das Flag data->shutdownFlag, dass die while-Schleifen der Fraktalberechnungen beendet werden sollen.[/Update]

Schon die Beispielapplikation QSimpleViewer aus dem ersten Teil des Tutorials erzeugt in der Diashow per QTimer eine gewisse Nebenläufigkeit, indem sie neben dem Warten auf Maus- und Tastatureingaben regelmäßig zum nächsten Bild blättert. Da es sich aber um keine echte Parallelisierung handelt, arbeitet sie sämtliche Events sequenziell ab. Bei einer einfachen Operation wie dem Weiterblättern von Bildern fällt das nicht weiter auf. Enthält der Event-Handler dagegen aufwendige Berechnungen oder I/O-Operationen, reagiert das Programm mit spürbaren Verzögerungen auf Eingaben des Benutzers. Das erste Beispiel QParallelDemo1 demonstriert diesen Effekt: Per Button wird hier mit einem möglichst ineffizienten Algorithmus die Berechnung einer Primzahl gestartet. Während der Ausführung der Funktion Gui::calc() reagiert das Programm auf keinerlei Events. Weder lässt sich das Datei-Menü aufrufen noch funktioniert das automatische Neuzeichnen des Fensters nach einer Überdeckung durch ein anderes Fenster.

Das Beispiel QParallelDemo2 zeigt, wie sich per Parallelisierung eine Verbesserung des Antwortverhaltens der Applikation erzielen lässt: Die Berechnung erfolgt dazu in einem separaten Thread, der parallel zur Event-Loop läuft. Zum Erzeugen eines neuen Threads dient bei Qt die Klasse QThread. Um eigenen Code auszuführen, leitet man eine Klasse von QThread ab und überlagert dann die virtuelle Funktion run(). Die Klasse QThread implementiert eine Reihe nützlicher Funktionen, Slots und Signale. Zum Starten des Threads dient die Slot-Funktion QThread::start(). Per QThread::isRunning() lässt sich abfragen, ob der Thread noch läuft. Bei Beendigung des Threads emittiert QThread das Signal QThread::finished(). Wie das genau funktioniert, ist im Beispiel zu sehen.

Die Klasse Calc ist von QThread abgeleitet und enthält neben dem Konstruktor nur die Funktion run() mit dem Code zum Berechnen der Primzahl. Dieser dient hier lediglich dazu, mit möglichst wenig Zeilen eine hinreichende Rechenlast zu erzeugen. Das Startsignal für den Thread gibt die Klasse Gui. Ihr Konstruktor erzeugt einen Start-Button, der mit der Funktion Gui::doCalc() verbunden wird.

Um das Neustarten des Calc-Threads vor dem Ende der Berechnung zu vermeiden, fragt der Aufruf calc->isRunning() zunächst den Status ab. Nur wenn der Thread gerade nicht läuft, wird er per calc->start() neu gestartet. Innerhalb von Calc beginnt dann sofort die Ausführung der Funktion run(). Sobald diese terminiert, emittiert die Klasse das von QThread geerbte Signal finished(), das in der Klasse Gui mit der Funktion Gui::calcFinished() verbunden ist.

Um mehrere Berechnungen gleichzeitig auszuführen, erzeugt der Konstruktor der Klasse Gui im Beispiel QParallelDemo3 gleich vier Calc-Objekte, die man dann wie im vorhergehenden Beispiel per Knopfdruck in der Funktion Gui::doCalc() startet. Zu jedem Thread gehört ein eigenes QLabel, das der Anzeige von Status und Ergebnis dient. Die Funktion Gui::calcFinished() zeigt in der ersten Zeile einen kleinen Trick, der immer dann nützlich ist, wenn mehrere Objekte gleichen Typs ein Signal mit nur einer einzigen Slot-Funktion verbinden. Hier liefert die in der Klasse QObject definierte Funktion sender() einen Zeiger auf das Objekt zurück, von dem das Signal stammt. Da das ein Zeiger auf QObject ist, wird an dieser Stelle noch eine Typumwandlung benötigt, die dann den Zugriff auf die in der Klasse Calc definierte Variable id ermöglicht.