Programmierkurs: Delphi: Pascal: Threads

Threads werden benötigt, um Programmteile parallel ablaufen zu lassen. So können verschiedene Aufgaben im Hintergrund gleichzeitig bearbeitet werden, z.B. das Sortieren einer Liste, die Übertragung von Daten über ein Netzwerk oder das Speichern von Spielständen in einer Online-Highscoreliste.

Allen Threads (zu deutsch etwa: Fäden) ist gemein, dass sie keine Eingaben vom Benutzer erwarten.

Um einen Thread zu erstellen, binden Sie in Ihr Programm bzw. in die Unit die Unit Classes ein. Darin ist die Klasse TThread deklariert, mit der wir nun arbeiten wollen.

Bei TThread handelt es sich um eine abstrakte Klasse. Dies bedeutet, dass diese zwar grundlegende Funktionen enthält, aber selbst noch nicht funktionsfähig ist. Der Programmcode, der in einem Thread ablaufen soll, kann ja in der Klasse TThread noch nicht enthalten sein. TThread enthält dafür die abstrakte Methode Execute, die wir in einem Nachkommen von TThread überschreiben und mit unserem gewünschten Code füllen müssen:

type
  TMyThread = class(TThread)
    procedure Execute; override;
  end;

procedure TMyThread.Execute;
begin
  { Threadcode }
end;

Kommunikation mit dem Hauptprogramm

Bearbeiten

Da Execute im Hintergrund abläuft, gibt es verschiedene Möglichkeiten, mit dem Thread zu kommunizieren. So zum Beispiel können Sie den Thread anhalten bzw. fortsetzen oder ganz abbrechen. Dazu bringt TThread die Methoden Suspend, Resume und Terminate mit. Über die Eigenschaften Suspended und Terminated lässt sich der jeweilige Status ermitteln.

Wenn Sie in Execute einen voraussichtlich zeitaufwändigen Vorgang ausführen, sollten Sie in regelmäßigen Abständen prüfen, ob die Eigenschaft Terminated auf True gesetzt ist. In diesem Falle ist die Schleife so bald wie möglich zu verlassen und die Methode Execute zu beenden. Auch alle von Execute aufgerufenen Methoden sollten regelmäßig den Status von Terminated überprüfen. So wäre es korrekt:

procedure TMyThread.Execute;
var i: Integer;
begin
  DateiOeffnen;
  for i := 0 to FeldAnzahl - 1 do
  begin
    LeseAusDatei(Feld[i]);
    if Terminated then Break;  // hier erfolgt der Abbruch der Schleife
  end;
  DateiSchließen;              // dieser Code wird auch bei Abbruch ausgeführt
end;

Über selbst deklarierte Ereignisse können Sie auch Zwischeninformationen an das Hauptprogramm weitergeben, wie den Fortschritt in Prozent oder Statusinformationen für die Fehlersuche.

Starten und Beenden

Bearbeiten

Einen Thread können Sie direkt beim Erstellen der Instanz oder erst später starten. Der Konstruktor nimmt dabei den Parameter CreateSuspended entgegen. Wenn Sie hier True übergeben, wird der Thread im pausierten Zustand erstellt, bei False hingegen wird der Thread gleich nach dem Erstellen gestartet:

MyThread := TMyThread.Create(False); // startet sofort
MyThread := TMyThread.Create(True);  // startet später
MyThread.Resume;                     // pausierten Thread starten

Sofern Sie keine Endlosschleife verwenden, endet der Thread automatisch, sobald der Programmcode von Execute endet. Wenn Sie den Thread zwischendurch anhalten möchten, rufen Sie die Methode Suspend auf. Um den Thread vorzeitig abzubrechen, verwenden Sie Terminate.

Ein einmal beendeter Thread lässt sich nicht wieder starten. Um den Code erneut auszuführen, müssen Sie den Speicher freigeben und das Threadobjekt erneut erstellen. Resume funktioniert nur, wenn der Thread pausiert erstellt oder zwischendurch angehalten wurde.

Weiterhin sollten Sie einen Start des Threads über den direkten Aufruf von Execute vermeiden, da Sie hiermit alle Kontrollmechanismen umgehen würden. Sie führen den Programmcode dann nicht in einem Extra-Thread, sondern im Hauptprogramm aus. Sie könnten ihn somit weder anhalten noch abbrechen, da das Hauptprogramm auf das Ende von Execute warten würde. Bei einer Endlosschleife bliebe nur ein erzwungener Abbruch des Hauptprogramm über den Task-Manager. Verwenden Sie also immer Resume.

Auf Beendigung eines Thread warten

Bearbeiten

Sobald ein Thread gestartet wurde, wird die Kontrolle wieder an das Hauptprogramm zurückgegeben und dieses setzt die Verarbeitung fort. Es kann nun also vorkommen, dass z.B. das Hauptprogramm beendet wird (von selbst oder durch den Benutzer), während im Hintergrund noch Threads ihre Arbeit verrichten. Wenn sie nichts unternehmen, werden die Threads abgebrochen. Sollen diese aber in jedem Falle ihre Arbeit beenden (z.B. eine Datei noch zuende schreiben), rufen Sie vor dem Freigeben des Speichers die Methode WaitFor auf:

MyThread.WaitFor;
MyThread.Free;

Hiermit ist sichergestellt, dass der Thread wirklich durchläuft und seine Arbeit beendet, bevor dessen Speicher freigegeben wird. WaitFor kann auch das Ergebnis des Threads zurückgeben. Abhängig davon können Sie weitere Aktionen veranlassen oder auch das Beenden des Programms verhindern, z.B. wenn der Thread nicht erfolgreich beendet wurde.

Synchronisation

Bearbeiten

Da Threads im Hintergrund ablaufen, können Sie das zeitliche Verhalten der Programmausführung nicht vorherbestimmen. Um keinen Datenmüll oder gar Programmabstürze zu erzeugen, gibt es verschiedene Möglichenkeiten, den Zugriff auf gemeinsam verwendete Daten zu schützen.

Die einfachste Methode ist, von einem Thread aufgerufene Objekte so zu programmieren, dass immer nur ein Aufruf möglich ist und weitere gesperrt werden. Der zweite Thread muss dann so lange warten, bis die Sperre aufgehoben wurde und er seine Änderungen vornehmen kann:

TMyObject = class
private
  FLocked: Boolean;
public
  procedure Lock;
  procedure Unlock;
  property Locked: Boolean read FLocked;
end;

procedure TMyThread.Execute;
begin
  // Warten, bis MyObject frei ist oder der Thread abgebrochen wurde
  while MyObject.Locked and not Terminated do;
  MyObject.Lock;
  { Änderungen an MyObject vornehmen }
  MyObject.Unlock;
end;

Ein Beispiel für diese Sperrung ist in der Klasse TCanvas enthalten, die eine Zeichenoberfläche für verschiedene Windows-Steuerelemente und für den Drucker zur Verfügung stellt. Es kann immer nur ein Thread zur Zeit auf ein TCanvas-Objekt zeichnen.

Bei der Programmierung grafischer Oberflächen unter Windows gibt es eine zweite Möglichkeit: Sie können bestimmte Aufrufe, die mit der Oberfläche interagieren (z.B. Textausgabe, Erweiterung von Listen, Füllen von Tabellen usw.) im Hauptthread ausführen lassen. Dazu übergeben Sie eine Methode des gleichen oder eines anderen Threads an die Methode Synchronize. Synchronize ist in TThread deklariert. Dieses nimmt sozusagen Ihre Anfrage in die Botschaftenliste von Windows auf und wartet so lange, bis Windows diese abgearbeitet hat. Da Synchronize auf Windows angewiesen ist, funktioniert es nicht in reinen Konsolenanwendungen und ist auch nur für Objekte der VCL (Visual Component Library) bestimmt.

Daneben existieren noch weitere Möglichkeiten: kritische Abschnitte, Synchronisierungsobjekte, die mehrfaches Lesen aber nur einfaches Schreiben erlauben sowie globale Threadvariablen. Eine umfassende Erläuterung würde hier jedoch zu weit führen. Bitte ziehen Sie gegebenenfalls die Delphi-Hilfe zu Rate.


  Pascal: Rekursion Inhaltsverzeichnis Pascal: Klassen