Als ersten Schritt erstellen wir erst einmal ein neues Projekt in der C++ Builder IDE.
Falls Sie mit dem Erstellen von Projekten und weiteren Grundlagen noch nicht vertraut
sind, lesen Sie bitte erst einmal das Kapitel über die C++ Builder Grundlagen, ebenfalls
in diesem Tutorial. Wie weiter oben bereits beschrieben, erstellen wir zusammen mit dem
Assistenten ein Thread-Objekt dem wir den Namen "TMyThread" zuweisen. Ihr Projekt sollte
nun im Projekt-Explorer folgendes Aussehen haben: Abb. 3: Projektstand
Als nächstes erstellen wir per Doppelklick auf den Schalter "Bsp. Ohne Thread" eine
Ereignis-Routine die wir folgendermaßen ausprogrammieren:
void __fastcall TForm1::StartButtonClick(TObject *Sender)
{ int i = 1; do
{
DisplayLabel->Caption = IntToStr(i);
i++;
} while (i < 200000);
}
Erzeugen Sie nun das Programm und führen es aus. Klicken Sie einmal auf den Schalter und
versuchen Sie nun, während das Programm rechnet, das Fenster zu verschieben bzw. es in der
Größe zu verändern und achten Sie auf die Anzeige des DisplayLabel! Richtig, während das
Programm rechnet, kann weder das Programm verschoben noch in der Größe verändert werden,
auch an der Anzeige des DisplayLabel tut sich überhaupt nichts. Ist die Berechnung vorbei
(dies kann je nach verwendeter Hardware differieren), zeigt der DisplayLabel den aktuellen
Wert von i an, standardmäßig 200 000.
Dieses Programm führt deutlich vor Augen, das dieses Programm so lange blockiert ist, bis
die Arbeit getan ist, erst danach kann der Anwender mit dem Programm weiterarbeiten. Damit
aber nicht jede Anwendung, die eine rechenintensive Funktion beherbergt, zur komplexen
Multithread Anwendung ausgebaut werden muss, schafft hier der Aufruf einer Funktion des
Application Objekts Abhilfe:
Application->ProcessMessages();
Dieser Aufruf sorgt nun dafür, dass nach jedem Schleifendurchlauf Windows die
Botschaftswarteschlange verarbeiten kann. Also auch die Botschaften, ob das Fenster
verschoben oder in der Größe verändert wird. Sogar der DisplayLabel wird nun aktualisiert.
Sind Threads also überflüssig, kann man alles mit diesem Aufruf erledigen? Nein, natürlich
nicht. Denn auch dieser Aufruf hat, wenngleich er einiges an Verbesserung der Situation
bietet, einen gewaltigen Pferdefuß.
Starten Sie die Berechnung noch einmal und verschieben Sie, während gerechnet wird, das
Formular auf dem Monitor. Was passiert? Während das Formular verschoben wird, wird die
Berechnung unterbrochen, selbst ein Klick auf das Schließen-Symbol wird nicht ausgeführt,
erst nachdem die Berechnung fertig ist. Auch könnte der Anwender mehrmals auf den Schalter
klicken, was hier zur Folge hätte, das die Berechnung mehrmals hinter einander durchgeführt
wird. Als Resultat können wir festhalten, das der Aufruf von "ApplicationProcessMessages"
zwar für kleinere Dinge wie das Neuzeichnen von Oberflächenelementen bzw. Empfangen der
Windows-Botschaften gute Dienste tut, jedoch nicht das liefert, was wir im Endeffekt
erreichen wollen.
Implementieren wir nun dieselbe Funktionalität innerhalb einer Thread-Klasse. Wechseln Sie
zur Klassendeklaration des neuen, abgeleiteten Threads und ergänzen Sie diese
folgendermaßen:
class TMyThread : public TThread
{ private:
int FCount; protected:
void __fastcall Execute(); //virtual, abstract muss implementiert werden. void __fastcall GuiElements(); //Methode für Synchronisation mit dem VCL-Hauptthread. public:
__fastcall TMyThread(bool CreateSuspended);
};
Die Originalklasse des Assistenten wird also um die Membervariable FCount und die
Methode Display() ergänzt. Die Variable benötigen wir als Durchlaufzähler und
Display() verwenden wir für die Aktualisierung der Anzeige des DiaplayLabel auf
Form1. Im Konstruktor setzen wir die Eigenschaft FreeOnTerminate = true,
somit wird der Thread freigegeben, sobald die Methode Terminate() aufgerufen wird:
void __fastcall TMyThread::GuiElements()
{ //Da wir uns nicht im Gültigkeitsbereich von Form1 befinden,
müssen wir qualifizieren mit Form1.
Form1->DisplayLabel->Caption = IntToStr(FCount);
}
Die Methode Execute() hat der Assistent bereits für uns angelegt. Es handelt sich hierbei
quasi um das Hauptprogramm eines Threads. Execute ist eine virtuelle Methode die in der
Basisklasse noch abstrakt ist. Wir ergänzen Sie nun:
void __fastcall TMyThread::Execute()
{
FCount = 1; do
{
Synchronize(&GuiElements);
FCount++;
} while (FCount <= 200000);
}
Wir wechseln nun nach Unit1 und fügen per Doppelklick auf den Schalter "Bsp.
mit Thread" der Unit eine neue Ereignis-Routine hinzu:
void __fastcall TForm1::StartThreadButtonClick(TObject *Sender)
{ new TMyThread(false);
}
Dieser Quelltext reicht aus, um den Thread zu starten. Sobald der Konstruktor aufgerufen
wird und diesem der Parameter false (für CreateSuspended) übergeben wird, startet
der Thread bzw. dessen Methode Execute().
Nun kommen wir zum Untersuchen des Programms. Kompilieren Sie alles und starten den Thread
führen Sie bei laufender Berechnung die selben Aktionen durch wie vorher.
Resultat 1: Wir können nun problemlos das Formular verschieben und in der Größe ändern,
die Berechnung wird fortgesetzt und die Anzeige ist immer aktuell.
Resultat 2: Ein Klick auf das Schließen-Symbol beendet die Anwendung unmittelbar.
Was kann man daraus folgern? Ganz einfach, das der Prozessor zwischen den beiden Threads
(VCL-Hauptthread und TmyThread) seine Zeit aufteilt und hin und her wechselt. Und da der
Prozessor ein verdammt flinkes Kerlchen ist, kommt es uns Menschen so vor, als wenn er
beide Aufgaben gleichzeitig durchführt (hierbei kommt es natürlich noch darauf an, was für
einen Prozessor man sein eigen nennt).
Um dieses hin und her wechseln, also das Aufteilen der Prozessorzeit sichtbarer zu machen,
können Sie die Schalterroutine ein wenig ausbauen:
void __fastcall TForm1::StartThreadButtonClick(TObject *Sender)
{ new TMyThread(false);
ShowMessage("VCL-Mainthread");
ShowMessage("VCL-Mainthread2");
}
Nach dem Start sehen Sie, dass sowohl der Thread ausgeführt wird, als auch das
Hauptprogramm (da während der Thread läuft, nacheinander beide MessageBoxen ausgegeben
werden). Damit der Thread aber auch sauber "zerstört/entfernt" wird, ergänzen wir die
Methode noch um folgende Zeile:
void __fastcall TForm1::StartThreadButtonClick(TObject *Sender)
{
FNewThread = new TMyThread(false); //Jetzt haben wir eine in der ganzen Klasse sichtbare Instanz.
FNewThread->OnTerminate = DeleteThread;
}
In der Methode, die dem Ereignis OnTerminate zugewiesen wird, kann der gesamte
Clean-Up-Code des Threads geschrieben werden.
void __fastcall TForm1::DeleteThread(TObject* Sender)
{
FNewThread->Terminate(); //Der Thread wird terminiert und freigegeben (FreeOnTerminate).
}
Mit diesen Zeilen stellen wir nun sicher, dass der Thread ordnungsgemäß entsorgt wird,
sobald die Zählervariable Fcount den Wert 200 000 erreicht hat. Wird die Anwendung aber
beendet, bevor dies geschehen ist, wird der Thread nicht freigegeben. Hierfür schreiben
wir noch etwas Code in das OnClose-Ereignis des Formulars:
Somit wäre dafür Sorge getragen, das der Thread sauber entsorgt wird. Setzen Sie doch
einmal einen Debugger-Haltepunkt in die Methode DeleteThread(...) und starten Sie den
Thread. Sie werden sehen, dass das Programm bei Erreichen von 200.000 sofort den Thread
terminiert und freigibt (da wir im Konstruktor FreeOnTerminate = true gesetzt haben).