Threads, die Arbeitstiere jeder Qt-Applikation, sorgen für ein reaktives GUI und ein verbessertes Benutzererlebnis – wenn sie die CPU parallel nutzen. Dazu muss der Entwickler sie mit einem Entwurfsmuster erst einmal bändigen, denn andernfalls hören sie nicht auf externe Kommandos.
Einem typischen Muster folgend wiederholt ein Workerthread eine ganz bestimmte Aufgabe so lange, bis der Entwickler ihn abbricht. Klassische Anwendung für Workerthreads sind Messwerterfassung, Berechnungen oder Maschinensteuerungen.
Qt erlaubt es seit der Version 4, das bekannte Signal-Slot-Konzept auch über Threadgrenzen hinweg zu nutzen [1]. Das setzt jedoch eine bestehende Eventqueue voraus. Aber genau die erschwert es, Workerthreads umzusetzen, da sie selbst eine Endlosschleife ist und damit jeden Code des Entwicklers blockiert. Abhilfe finden jene, die einen eigenen Workerthread implementieren, der durch Signals und Slots ansprechbar bleibt. Ist der nach modernen Designprinzipien entworfen, lässt er sich nahtlos in eigene Projekte integrieren.
Um Threads in Qt zu implementieren, definieren Entwickler typischerweise eine Klasse, die von »QThread« ableitet. Den eigentlichen Code, der parallel ablaufen soll, implementieren sie in der überschriebenen Methode »QThread::run()«.
Implementierung in Qt
Der so programmierte Thread läuft durch den Aufruf der Methode »QThread::start()« los. Die Klasse »QThread« ist intern so implementiert, dass sie auf jeder Zielplattform von Qt den jeweils nativen Code zur Threadverwaltung nutzt. Der Workerthread führt eine bestimmte Codefolge fortlaufend aus, zum Beispiel in einer While-Schleife.
In Verbindung mit Qt und der Anforderung, weiterhin auf Signale und Slots zu reagieren, entsteht ein Problem: Möchte der Entwickler, dass sein Threadcode auf Signale antwortet, muss er aus der Methode »run()« heraus »exec()« aufrufen. Erst dann startet die Eventloop und der Thread reagiert auf Signale. Da die Qt-Entwickler die Methode »exec()« jedoch selbst als Endlosschleife implementiert haben, ist es nicht unmittelbar möglich, einen Workerthread zu implementieren, der weiterhin auf Signale reagiert.
Ein zusätzliches Problem taucht auf, wenn der Programmierer ein Signal aus einem Thread mit dem Slot eines anderen Thread verbinden will: In Qt besitzt jedes Objekt eine so genannte Threadaffinität [2]. Das bedeutet, dass es immer in dem Thread lebt, in dem sein Konstruktor aufgerufen wurde. Die CPU führt den Code von Slot-Methoden immer im Kontext dieses Thread aus. Entwickler würden erwarten, dass eine per »connect()« verbundene Slot-Methode im Kontext des neuen Thread läuft. Tatsächlich führt Qt den Code der Slot-Methode jedoch im Kontext des Hauptthread aus. Das liegt daran, dass Qt das »QThread«-Objekt, das den neuen Thread repräsentiert, weiterhin im Hauptstrang ausführt.
Doch findige Entwickler wissen sich zu helfen und kennen die Methode »moveToThread()«. Damit vermögen sie die Threadaffinität eines Objektes zu ändern. Aufgrund der internen Implementierung gelingt es aber nur, ein Objekt vom eigenen Thread in einen anderen zu schieben. Eine andere Verwendung verhält sich undefiniert und führt im schlimmsten Fall zu einem Absturz der Anwendung.
Zwei Threads im Gespräch
Seit Qt 4.0 erlaubt die Bibliothek, dass Threads über Signale und Slots kommunizieren und so Daten miteinander austauschen. Dabei müssen Entwickler jedoch eine Besonderheit der »connect()«-Anweisung beachten: Der optionale fünfte Parameter definiert, wie die Eventqueue die Signale an das empfangende Objekt ausliefert. Tabelle 1 listet die gültigen Werte für den Parameter auf.
|
Tabelle 1: Varianten |
|
|---|---|
|
fünfter Parameter |
Funktion |
|
“Qt::AutoConnection” (Standard) |
Liegt das Empfängerobjekt im selben Thread wie das |
|
“Qt::DirectConnection” |
Umgeht die Eventqueue vollständig. Der “emit()”-Aufruf |
|
“Qt::QueuedConnection” |
Legt das Signal zunächst in der Eventqueue des |
|
“Qt::BlockingQueuedConnection” |
Wie “Qt::QueuedConnection”, der “emit()”-Aufruf beim Sender |
|
“Qt::UniqueConnection” |
Neu seit Qt 4.6: Wie “Qt::AutoConnection”, fügt die |
|
“Qt::AutoCompatConnection” |
Qt-3-Klassen verwenden diesen Parameter hauptsächlich. Er |
Das Problem liegt im Verhalten der Vorgabe durch »Qt::AutoConnection«: Manchem Entwickler unterläuft der Flüchtigkeitsfehler, Signal-Slot-Verbindungen zwischen Threads nicht explizit mit »Qt::QueuedConnection« zu definieren. Dies bereitet mitunter Probleme: Stellt der Entwickler vom Hauptthread eine Verbindung zu einem anderen Threadobjekt her, liegen sowohl das Sender- als auch das Empfängerobjekt im selben Ablaufstrom. Dies hat zur Folge, dass das Standardverhalten von »connect()« einen direkten Funktionsaufruf implementiert. Dadurch führt Qt den Code der Slot-Methode im Kontext des aufrufenden Thread, also nicht dem Zielthread, aus. Explizit eine »Qt::QueuedConnection« anzufordern löst das Problem.
Parallelarbeit auslagern
Ein aus mehreren Klassen bestehendes Projekt setzt diesen Weg um (siehe Abbildung 1). Listing 1 enthält das Hauptprogramm und instanziert einen »WorkerThread«. Weil er das Interface eines »AbstractWorkerType« aus Listing 2 implementiert, versteht er in Zeile 13 von »main()« die Methode »startThread()«. Diese erwartet ein Objekt, das den parallel abzuarbeitenden Code enthält. Wer diese Lösung in einem eigenen Projekt verwendet, setzt durch die Methode »processWork()« dieses Interface um.

Abbildung 1: Das UML-Diagramm verdeutlicht den Zusammenhang zwischen den beteiligten Klassen. Qt-Programme legen einen »WorkerThread« an und übergeben ihm ein Objekt, das zu parallelisierende Arbeit erledigt. Dies ist hier der »DebugWorker«, der das Interface »AbstractWorkType« implementiert. Das »WorkerThreadPrivate«-Objekt ist für den Entwickler unsichtbar und nutzt intern den »DummyThreadAffinityType«.
|
Listing 1: |
|---|
01 #include <QtCore>
02
03 #include "workerthread.h"
04 #include "debugworker.h"
05
06 int main(int argc, char** argv)
07 {
08 QCoreApplication a(argc, argv);
09
10 qDebug() << "Main thread id:"
11 << QThread::currentThreadId();
12
13 WorkerThread* thread1 = new WorkerThread();
14 thread1->startThread(new DebugWorker());
15
16 return a.exec();
17 }
|
|
Listing 2: |
|---|
01 #ifndef ABSTRACTWORKTYPE_H
02 #define ABSTRACTWORKTYPE_H
03
04 class AbstractWorkType
05 {
06 public:
07 virtual void processWork() = 0;
08 };
09 #endif // ABSTRACTWORKTYPE_H
|
Hilfsklasse spielt über Eck
Listing 3 enthält die Header zum exemplarisch in Listing 4 implementierten »DebugWorker«. Er alloziert in einer Endlosschleife 80 MByte Daten auf dem Heap, befüllt sie mit Werten und gibt sie wieder frei. So erhält der Entwickler die Chance, ihre Konsolenausgaben zu verfolgen (siehe Abbildung 2).

Abbildung 2: Einmal übersetzt ruft die Hauptroutine einen beispielhaften »DebugWorker« immer wieder auf. Er läuft in einem separaten Thread gegenüber »Main«, nimmt aber trotzdem Signale entgegen.
|
Listing 3: |
|---|
01 #ifndef DEBUGWORKER_H
02 #define DEBUGWORKER_H
03
04 #include <QtCore>
05 #include "abstractworktype.h"
06
07 class DebugWorker : public AbstractWorkType
08 {
09 public:
10 DebugWorker();
11 void processWork();
12
13 private:
14 unsigned int runCounter;
15 };
16 #endif // DEBUGWORKER_H
|
|
Listing 4: |
|---|
01 #include "debugworker.h"
02
03 DebugWorker::DebugWorker()
04 {
05 runCounter = 0;
06 }
07
08 void DebugWorker::processWork()
09 {
10 qDebug() << "Working in Thread:" << QThread::currentThreadId();
11 // Attention: We allocate nearly 80 Megabytes of memory to slow
12 // down the thread for demonstration purposes!
13 double* workLoad = new double[10000000];
14 for (int i = 0; i < 10000000; i++)
15 {
16 workLoad[i] = (3.141 * (double)i) / 2.241;
17 }
18 delete[] workLoad;
19
20 qDebug() << "Finished run:" << runCounter;
21 runCounter++;
22 }
|
Um in den Listings 5 und 6 die Klasse »WorkerThread« zu implementieren, verwendet der Code zwei Konzepte: Erstens nutzt er intern die Klasse »WorkerThreadPrivate« (siehe Listing 7). Sie stellt den eigentlichen Thread dar. Sobald dieser startet, legt Qt als Dummy das »ThreadAffinityObject« an. Es bildet später die Referenz dafür, um in Zeile 18 von Listing 6 die Affinität des Fadens zu ändern.
|
Listing 5: |
|---|
01 #ifndef WORKERTHREAD_H
02 #define WORKERTHREAD_H
03
04 #include <QtCore>
05 #include "workerthreadprivate.h"
06
07 class WorkerThread : public QObject
08 {
09 Q_OBJECT
10
11 public:
12 WorkerThread();
13 void startThread(AbstractWorkType*);
14
15 private:
16 WorkerThreadPrivate* workerThread;
17
18 private slots:
19 void threadStarted();
20
21 signals:
22 void ready();
23 };
24 #endif // WORKERTHREAD_H
|
|
Listing 6: |
|---|
01 #include "workerthread.h"
02
03 WorkerThread::WorkerThread()
04 {
05 workerThread = new WorkerThreadPrivate();
06 connect(workerThread, SIGNAL(started()),
07 this, SLOT(threadStarted()));
08 }
09
10 voidWorkerThread::startThread(AbstractWorkType* workType)
11 {
12 workerThread->setWorkType(workType);
13 workerThread->start();
14 }
15
16 void WorkerThread::threadStarted()
17 {
18 workerThread->moveToThread(
19 workerThread->getThreadAffinityObject()->thread());
20 workerThread->setupWorkConnection();
21
22 emit ready();
23 }
|
Zweitens sorgt das Verwenden des State-Pattern [3] in dieser Klasse dafür, zukünftig eine universelle Lösung zur Hand zu haben: Programmierer können sie problemlos auch in anderen Projekten wieder verwenden. Listing 8 schließlich enthält die Implementation dieses Verbindungsglieds.
|
Listing 7: |
|---|
01 #ifndef WORKERTHREADPRIVATE_H
02 #define WORKERTHREADPRIVATE_H
03
04 #include <QtCore>
05 #include "abstractworktype.h"
06
07 class DummyThreadAffinityType : public QObject {};
08
09 workerthreads
10 class WorkerThreadPrivate : public QThread
11 {
12 Q_OBJECT
13
14 public:
15 WorkerThreadPrivate();
16
17 void setupWorkConnection();
18
19 void setWorkType(AbstractWorkType*);
20
21 DummyThreadAffinityType* getThreadAffinityObject() {
22 return threadAffinityObject;
23 }
24
25 protected:
26 void run();
27
28 private:
29 AbstractWorkType* work;
30 DummyThreadAffinityType* threadAffinityObject;
31
32 private slots:
33 void processWork();
34
35 signals:
36 void scheduleWork();
37 };
38 #endif // WORKERTHREADPRIVATE_H
|
|
Listing 8: |
|---|
01 #include "workerthreadprivate.h"
02
03 WorkerThreadPrivate::WorkerThreadPrivate()
04 {
05 }
06
07 void WorkerThreadPrivate::setupWorkConnection()
08 {
09 connect(this, SIGNAL(scheduleWork()),
10 this, SLOT(processWork()),
11 Qt::QueuedConnection);
12
13 emit scheduleWork();
14 }
15
16 void WorkerThreadPrivate::setWorkType(AbstractWorkType* workType)
17 {
18 work = workType;
19 }
20
21 void WorkerThreadPrivate::run()
22 {
23 qDebug() << "WorkerThreadPrivate::run() in Thread:"
24 << QThread::currentThreadId();
25
26 threadAffinityObject = new DummyThreadAffinityType();
27
28 exec();
29 }
30
31 void WorkerThreadPrivate::processWork()
32 {
33 qDebug() << "WorkerThreadPrivate::processWork() in Thread:"
34 << QThread::currentThreadId();
35
36 if (work != 0)
37 {
38 work->processWork();
39 }
40
41 emit scheduleWork();
42 }
|
Instanzen verdrahten
Einen neuen Workerthread legt der Entwickler an, indem er die gleichnamige Klasse instanziert. Um ihn zu starten, übergibt er der Methode »startThread(AbstractWorkType*)« ein konkretes »WorkType«-Objekt. Die Methode aktiviert damit den tatsächlichen Thread, der die notwendige interne Verbindung mittels »setupWorkConnection()« aufbaut. Sie simuliert das Verhalten einer Endlosschleife. Die Methode »processWork()« ruft die Implementierung des konkreten »WorkType«-Objekts auf und wiederholt diesen Vorgang über die eingerichtete Signal-Slot-Verbindung (siehe Listings 5 und 6).
Interessierte Entwickler dürfen den Code aus einem Git-Repository erweitern, anpassen oder verbessern [4]. Um das Beispiel nachzuvollziehen, installiert sich der Entwickler in einer aktuellen Linux-Distribution Qt 4 [5] und das freie Versionskontrollsystem Git [6]. In einem Terminal lädt das Kommando
git clone git://github.com:picaschaf/U Qt-Worker-Thread.git
das aktuelle Repository herunter. Nach dem Wechsel in das neue Verzeichnis mit »cd Qt-Worker-Thread« ruft der geübte Qt-Entwickler das bekannte Buildkommando »qmake && make« auf.
Ansprechbar bleiben
Seit Qt 4 dürfen Entwickler Threads verwenden. Jedoch erlaubt es nicht einmal die aktuelle Version 4.6 des Framework, einen über Signale ansprechbaren Workerthread zu schreiben. Entwicklern bleibt nur, das Verhalten in eigenem Code nachzubilden und Qts Threading-Konzept auszutricksen. Andernfalls führt die Bibliothek den Code nicht parallel, sondern direkt im Hauptthread aus. Durch diesen Workaround verlangsamt sich die Lösung im Vergleich zu einem klassischen Workerthread – dafür darf er in seinem aufgepeppten Thread mittels Signal und Slots Methoden ausführen: So an der Longe geführt sind Qt-Anwendungen leicht im Zaum zu halten. (mg)
|
Infos |
|---|
|
[1] Threads in Qt 4.6: [http://doc.trolltech.com/4.6/threads.html] [2] Veränderbare Zuordnung eines Objekts zu einem Thread: [http://doc.qt.nokia.com/4.6/qobject.html#moveToThread] [3] Entwurfsmuster State-Pattern:[http://de.wikipedia.org/wiki/Zustand_(Entwurfsmuster)] [4] Git-Repository der Programmierbeispiele:[http://github.com/picaschaf/Qt-Worker-Thread] [5] Qt von Nokia: [http://qt.nokia.com] [6] Sourcecode-Management mit Git:[http://www.git-scm.com] |
|
Der Autor |
|---|
|
Alexander Nassian programmiert in C/C++, Qt und C# für Mono. Er arbeitet als Software-Entwickler und Trainer für die Hilf GmbH in Oberhaching bei München. |






