Plattformunabhängige Parallelprogrammierung mit C++ und Qt, Teil 4: Vom QThreadPool zum QFutureWatcher

Neben Klassen für das Erzeugen und Synchronisieren von Threads bietet Qt auch Konstrukte, mit denen sich Nebenläufigkeit im Thread-Pool der Applikation und auf Funktionsebene implementieren lässt.

In Pocket speichern vorlesen Druckansicht 1 Kommentar lesen
Lesezeit: 14 Min.
Von
  • Matthias Nagorni
Inhaltsverzeichnis

Neben Klassen für das Erzeugen und Synchronisieren von Threads bietet Qt auch Konstrukte, mit denen sich Nebenläufigkeit im Thread-Pool der Applikation und auf Funktionsebene implementieren lässt.

Die bisherigen Artikel des Tutorials haben gezeigt, wie sich Threads erzeugen und synchronisieren lassen. Der vorliegende Teil knüpft daran an und erklärt zunächst, wie man Signal-Slot-Verbindungen über Thread-Grenzen hinweg nutzt. Threads haben den Vorteil, dass sie Tuning auf Betriebssystemebene ermöglichen, was insbesondere für Echtzeitanwendungen von Bedeutung sein kann. Für viele Anwendungen ist es aber einfacher, den Thread-Pool der Applikation zu nutzen und Nebenläufigkeit auf Funktionsebene zu implementieren.

Qt bietet daher neben Threads weitere Optionen für das Schreiben parallelen Codes. Beispielsweise lässt sich eigener Code auf Basis der Klasse QRunnable im Thread-Pool der Applikation ausführen. Für die parallele Ausführung von Funktionen verfügt der Namensraum QtConcurrent neben einer run- auch über MapReduce- und Filter-Funktionen.

Nebenläufigkeit lässt sich mit Qt entweder anhand von eigenen Threads oder durch Nutzung des Threadpools mit QRunnable und QtConcurrent implementieren (Abb. 1).

Seit Qt[]4.4 gibt es zwei Ansätze zur Verwendung der Klasse QThread. Das ältere Verfahren, eine eigene Klasse von QThread abzuleiten und dessen run-Funktion zu reimplementieren, ergänzt ein Ansatz, bei dem QThread als zusätzliche Event-Loop dient. Um die Bearbeitung von Events einem bestimmten Thread zuzuordnen, verwendet man bei letzterer Variante die Funktion moveToThread der Klasse QObject. Der folgende Codeausschnitt nutzt die Standardimplementierung von QThread, dessen run-Funktion per exec() die Event-Loop im Thread startet:

thread = new QThread;
object = new Object;
object->moveToThread(thread);
thread->start();
Mehr Infos

Plattformunabhängige Parallelprogrammierung mit C++ und Qt

Den Quellcode zum Artikel findet man hier.

Den zu parallelisierenden Code implementiert der Entwickler in Form von Slot-Funktionen in der Klasse Object. Es ist erlaubt, Signale über Thread-Grenzen hinweg zu senden. Da nicht immer gewährleistet ist, dass der Ziel-Thread gerade die Event-Loop ausführt, bietet Qt mit Qt::ConnectionType mehrere Optionen für Signal-Slot-Verbindungen, die sich beim Aufruf von QObject::connect als Parameter übergeben lassen. Standardmäßig führt Qt die Slot-Funktion unmittelbar aus, wenn ihre Thread-Affinität im Thread des Senders liegt. Falls Signal und Slot unterschiedliche Thread-Affinität haben, schaltet Qt gemäß der Voreinstellung automatisch eine Queue dazwischen, sodass die Slot-Funktion startet, sobald der Ziel-Thread seine Event-Loop ausführt.

Das Beispiel QParallelObjects demonstriert das anhand der timeout-Signale zweier QTimer. Den Timer der Klasse Thread verbindet das Programm mit der Slot-Funktion Thread::count(). Wie das Beispiel zeigt, läuft die Funktion in der Event-Loop der Applikation, unabhängig davon, ob der Thread gestartet wurde. Die Slot-Funktion Object::count() wird dagegen in der Event-Loop des Thread ausgeführt. Entsprechend arbeitet Qt die Timer-Events in der Klasse Object nur ab, wenn der Thread läuft. Dass hier eine sogenannte Queued Connection vorliegt, erkennt der Leser daran, dass beide Zähler den gleichen Zählerstand zeigen, sobald der Thread gestartet ist.

Während es ungefährlich ist, aus einem Thread Signale zu senden, muss der Entwickler bei der Implementierung von Slot-Funktionen in von QThread abgeleiteten Klassen Vorsicht walten lassen. Dies ist nicht Thread-sicher, da das Programm ja die Slot-Funktionen der Thread-Klasse im Haupt-Thread der Applikation ausführt. Entsprechend sind die Variablen der Thread-Klasse gegebenfalls per Mutex vor gleichzeitigem Zugriff durch Haupt-Thread und Thread zu schützen.

Da die Synchronisierung von Zugriffen auf geteilte Variablen zu Lasten der Performance geht, ist es in vielen Fällen vorteilhaft, lokalen Thread-Speicher (engl. thread-local storage) zu nutzen. Unter Qt verwendet man dafür die Template-Klasse QThreadStorage. Um ihr ein Datenobjekt vom Typ T zuzuweisen, nutzt man die Funktion

void QThreadStorage::setLocalData( T data ) 

Falls T ein Pointer-Typ ist, ist data zunächst auf dem Heap zu erzeugen. QThreadStorage übernimmt dann die Verwaltung und gibt den für data allokierten Speicher beim Ende des Thread automatisch frei. Der Zugriff auf den lokalen Speicher erfolgt mit den localData()-Funktionen, die bei Pointer-Typen eine Referenz auf das Objekt und bei Value-Typen eine Kopie liefern:

T & QThreadStorage::localData ()
T QThreadStorage::localData () const

Beim Verwenden von QThreadStorage ist zu beachten, dass dessen Destruktor den lokalen Speicher nicht freigibt. Daher darf man ein QThreadStorage-Objekt erst löschen, wenn sämtliche damit verbundenen Threads beendet sind.