Multithreading

Vorwort

In diesem Tutorial versuche ich euch etwas in das Mysterium der Threads einzuweihen. Ich werde versuchen euch die Leistungsfähigkeit und die Gefahren näher zu bringen. Um besser zu verstehen wann es Sinn macht Threads einzusetzen und wann nicht.

Was ist ein Thread?

Als Erstes sollten wir einmal klären was ein Thread überhaupt ist. Ein Thread ist die Möglichkeit einen Code so auszuführen, dass er zur "gleichen Zeit" ausgeführt wird wie anderer Code unserer Anwendung. Allerings hier haben wir Trugschluss #1. In echt werden die Quellen nicht zur gleichen Zeit ausgeführt. Sondern alle Threads werden vom Betriebssystem abwechselnd ausgeführt. Erst kommt Thread 1 dran. Dieser wird allerdings nach ein paar Millisekunden wieder unterbrochen um einen Anderen ran zu lassen. Wenn niemand sonst etwas brechnen will kommt sofort wieder Thread 1 dran. Wollen 2 Threads gleichzeitig etwas berechnen teilen diese sich die Zeit zu ca. 50%.

Ist das wirklich immer so? Nein. Der Trend geht dorthin, dass die Prozessoren 2, 3 oder 4 Kerne enthalten. Diese Kerne sind nichts anderes als das was früher komplette Prozessoren waren. Kleine seperate Recheneinheiten. Dadurch, dass diese Recheneinheiten voneinander getrennt sind ist es dort möglich 2, 3 oder 4 Quellcodes wirklich gleichzeitig ausführen zu können.

Neben den Prozessoren mit mehreren Kernen gibt es noch welche mit einer Technik namens Hyperthreading. Diese Technik ermöglicht es auch mehrere Codes nahezu gleichzeitig ausführen zu können. Nahezu deswegen, da beim Hyperthreading die internen Abläufe einer Recheneinheit optimiert werden. Jedes Mal, wenn Thread 1 auf externe Hardware (Hauptspeicher etc.) warten muss, dann wechselt die Recheneinheit automatisch zu Thread 2 oder wieder zurück zu Thread 1. Dadurch ist möglich die Recheneinheit besser auszulasten. Der Geschwindigkeitsvorteil hängt beim Hyperthreading aber extrem stark von dem auszuführenden Quellcode ab.

Außerdem gibt es noch sogenanntes Multprocessing. Der Unterschied zum Threading liegt darin, dass mehrere Anwendungen gestartet werden. Diese haben dann einen komplett getrennten Speicherbereich, was beim Multithreading nicht immer der Fall ist. Aus diesem Grund muss der gemeinsam genutzte Speicher auch entsprechend geschützt werden. Wie das geht erkläre ich später im Abschnitt "Synchronschwimmen".

Der Vollständigkeithalber sollte noch erwähnt werden, dass jede Anwendung immer zwingend einen Thread hat. Da Windows zum Beispiel nur Rechenleistung auf Threads verteilt könnte eine Anwenung ohne einen einzigen Thread nicht funktionieren. In Delphi nennt sich dieser Thread auch VCL-Thread. Der ist unter anderem dafür da um Botschaften von Windows zu verarbeiten. Als Entwickler wird man nie direkt mit diesem Thread zu tun haben aber nichts desto trotz ist er vorhanden.

Wozu brauche ich Threads denn überhaupt?

Das typische Einsatzgebiet von Threads ist überall dort wo:

  • 1. mehrere Quellen gleichzeitig ausgeführt werden müssen. z.B.: Wenn man ein Spiel programmiert in dem im Hintergrund schon ein neuer Level geladen wird. Ein Beispiel dafür ist Half-Life. Am Ende eines Levels erscheint ein Schriftzug "Loading" und es ruckelt dann ein wenig. Und genau zu diesem Zeitpunkt wird im Hintergrund ein neuer Level geladen. So hat man den Eindruck es handele sich dabei um ein riesiges Level. Viele aktuellen Spielen wären ohne so eine Technik nicht möglich.
  • 2. man auf Hardware warten muss (Modem) z.B.: ein Webspider. Er lädt Webseiten auf die Festplatte. Dort muss die Anwendung größtenteils auf die Server warten. An dieser Stelle haben Threads 2 große Vorteile:
    • 1. Solange auf den Server gewartet werden muss blockiert die Anwendung nicht (da nur der eine Thread wartet).
    • 2. Und der wohl größere Vorteil. Es wird ermöglicht, mehrere Dateien gleichzeitig herunter zu laden.

    So kann man in kürzerer Zeit und effizienterer Nutzung der Internetanbindung (DSL, ...) eine oder mehrere Webseiten herunter laden.

    • 3. man wirklich komplexe Berechnungen durchführen muss. Wenn man solche Berechnungen auf 2-4 Threads aufteilt, dann wird die benötigte Zeit deutlich reduziert, vorhandene Recheneinheiten effektiv ausgenutzt werden. Außerdem auch hier reagiert die Anwendung jederzeit vernünftig und wird nicht blokiert.

Wie überall wird der Kreativität aber kaum eine Grenze gesetzt.

Wie kann ich sie denn erstellen?

Es gibt mehrere Möglichkeiten die Threads zu erstellen. Ich gehe hier nur auf die Einfachste und Komfortabelste ein. Das Einzige was wir dafür tun müssen ist eine Klasse von TThread abzuleiten und dort die Methode Execute zu überschreiben.

Wem objekt orientiertes Programmieren (OOP) Probleme bereitet der sollte an genau dieser Stelle aufhören und lieber erst einmal ein Tutorial darüber lesen.

interface
 
uses
  classes;
 
type
  TMyOwnThread = class(TThread)
  protected
    procedure Execute; override;
  end;
 
implementation
 
procedure TMyOwnThread.Execute;
begin
  // Führe hier irgendwelche Berechnungen aus.
end;

Und was war da jetzt so kompliziert dran? Bisher noch nichts. Das Komplexe steckt wie immer Detail. Man kann im vorherein nie alle Fehler ausschließen. Das ist nur Möglich, wenn man vorher jede noch so kleine Situation durchspielt. Wenn innerhalb dieser Methode ein Fehler passiert, dann wird von Delphi eine Exception ausgelöst. Normalerweise werden Exceptions von dem Applicationobjekt von der VCL abgefangen. Aber über einem zusätzlichen Thread hat dieses Objekt keine Gewalt. Entsprechend landet die Exception direkt beim Betriebssystem. Das Betriebssystem wird sich dann bei euch mit einem "Unknown Software Error" bedanken und anschließend vermutlich auch noch eure Anwendung komplett beenden.

Da das selten Sinn und Zweck einer Anwendung ist müssen wir dagegen etwas unternehmen. Also müssen wir den Fehler entsprechend abfangen und darauf reagieren. Wie man auf den Fehler reagiert kommt immer auf den einzelnen Fall darauf an. Eventuell genügt es die Berechnung als Fehlerhaft zu kennzeichnen oder aber der Fehler muss detailiert Protokoliert werden.

procedure TMyOwnThread.Execute;
begin
  try
    // Führe hier irgendwelche Berechnungen aus.
  except
    on e: exception do begin
      // mache hier irgendetwas mit dem Fehler.
    end;
  end;
end;

Soviel zur Vorbereitung. Jetzt wollen wir den Thread aber auch ausführen. Dazu brauchen wir zu erst eine Methode in der wir diesen Thread erzeugen können. Nehmen wir mal das Event Form1.OnCreate.

procedure TForm1.FormCreate(Sender: TObject);
var
  Thread: TMyOwnThread;
begin
  Thread := TMyOwnThread.Create(True);
  // Der Parameter heißt CreateSuspended.
  // Er hat zur Folge wenn wir ein false übergeben,
  // dass der Thread sofort anfängt mit arbeiten.
  // meist haben wir ihm dann aber noch gar keine Daten übergeben.
  // also rufen wir ihm Suspended auf
 
  Thread.FreeOnTerminate := True;
  // FreeOnTerminate bedeutet sobald der Thread die Procedure Execute
  // verlassen hat wird sein Speicher wieder von alleine Frei gegeben.
  // andernfalls müsste später im Programm Thread.Free aufgerufen werden.
 
  Thread.Resume;
  // Falls der Thread suspended gestartet wurde sorgt dies dafür,
  // dass er anfängt mit arbeiten.
 
  Thread.Suspend;
  // Dies sorgt dafür, dass die Arbeit des Threads pausiert wird.
  // Weiteführung durch Resume.
 
  Thread.Terminate;
  // Hierbei handelt es sich nicht um eine Methode die dafür sorgt,
  // dass der Thread aufhört zu arbeiten. Sondern sie setzt eine Variable
  // (FTerminated) in der Basisklasse.
  // Ein Thread muss auf diese Methode selber reagieren.
  // Wenn in Execute Berechnungen in einer Schleife durchgeführt werden,
  // dann muss die Property Terminated abgefragt werden
  // und wenn diese gesetzt ist, dann sollte die arbeit
  // normal beendet werden und die Methode verlassen werden.
end;

Das war es eigentlich schon soweit zum Thema Thread erstellen. Ach ja noch eines. Der Thread ist ansonsten genau das Selbe wie jede andere Klasse auch. Sprich er kann genau so erweitert werden wie das Form1 oder sonst irgend eine Klasse.

Synchronschwimmen (oder auch wo bin ich) ...

Das wohl komplizierteste an Multithreading ist zu wissen in welchem Thread (Kontext) eine Methode aufgerufen wird und wann bzw. was man sie synchronisieren oder sie schützen sollte. In diesem Abschnitt versuche ich das euch einmal näher zu erklären.

Um schon einmal den wohl am häufigst gemachten Irrtum aus der Welt zu schaffen. Ein Thread existiert erst genau dann, wenn die Execute Methode aufgerufen wurde. Und dort meine ich nicht, dass man irgendwo im Quelltext Thread.Execute stehen hat. Nein ich meine den resultierenden Aufruf vom Betriebssystem auf Thread.Resume. Da das bestimmt ein wenig unverständlich war hier mal ein paar Beispiele aus dem FormCreate (VCL-Thread).

Thread := Thread.Create(True);
// Der Konstructor wird aus dem VCL-Thread aufgerufen
// es existiert noch kein Thread!
 
Thread.Resume;
// Hiermit wird der Thread angeworfen und sobald
// die erste Zeile von Execute aufgerufen
// wurde existiert der Thread
 
Thread.Suspend;
// Es handelt sich zwar hier um eine Methode der Threadklasse
// allerdings wird sie im Kontext vom VCL-Thread aufgerufen.
// Der Thread wird hierbei aber vom Betriebssystem angehalten.
// Resume kann diesen dann jederzeit wieder starten.
 
Thread.Terminate;
// Genau da Selbe wie bei Suspend allerdings wird er hierdurch
// nicht angehalten.
// sondern es wird die schon besprochene Variable gesetzt.
 
Thread.Execute;
// Die Execute Methode wurde nicht vom Betriebsssytem aufgerufen
// und wird somit im Kontext vom VCL-Thread aufgerufen.
// Es existiert KEIN Thread.
 
// Das funktioniert allerdings nur sofern diese als Public
// überschrieben wurde. Was natürlich nicht gemacht werden sollte!!!

Jetzt mal ein Beispiel aus dem Thread.Execute.

procedure TMyOwnThread.Execute;
begin
  Thread.Terminate;
  // Diese Methode wurde nun aus dem Kontext vom den Thread
  // aufgerufen und setzt die bekannte Variable.
 
  Form1.Button1OnClick();
  // Hierbei handelt es sich zwar um eine Methode aus dem VCL-Thread
  // (Form1) aber sie wird im Kontext von unserem Thread aufgerufen.
  // Es spielt überhaupt keine Rolle wie die Methode heißt oder
  // was sie macht. Es könnte sich hierbei auch um einen Callback
  // vom Thread handeln.
end;

Alles was aus dem Execute aufgerufen wird hat als Kontext diesen Thread!

Jetzt stellt sich natürlich die Frage was muss alles synchronisiert werden? Pauschal lässt sich nur sagen, alles was irgendwo von mehreren Threads (incl. VCL-Thread) geschrieben werden kann muss synchronisiert werden. Ich erkläre euch das mal besser an einem kleinem Beispiel.

Nehmen wir einmal an wir haben einen Webspider. Dieser soll 200 Webseiten downloaden. Wir erstellen uns einen Thread dem ich die URL übergebe und der selbständig mit laden anfängt. Wenn dieser fertig ist dann sagt er dm VCL-Thread mittels eines Events, dass er fertig ist. Aussehen würde das so.

type
  TBinFertig = procedure(const Content: String) of object;
 
  TMyOwnThread = class(TThread)
  private
    FBinFertig: TBinFertig;
    procedure SyncBinFertig;
  public
    property BinFertig: TBinFertig read FBinFertig write FBinFertig;
  end;
 
implementation
 
procedure TMyOwnThread.SyncBinFertig;
begin
  if Assigned(FBinFertig)
    then FBinFertig(DasIstDerInhaltDerWebseite);
end;
 
procedure TMyOwnThread.Execute;
begin
  try
    // Download der Seite ...
 
    // Synchronisieren
    Synchronize(SyncBinFertig);
  except
    on e: exception do begin
      // mache hier irgendetwas mit dem Fehler.
    end;
  end;
end;

So lasst das Stück Quelle einmal auf euch wirken. Ich erkläre in der Zeit was genau wo passiert und warum das so aussieht. TBinFertig ist die Definition einer Objektmethode. Das ermöglicht es Proceduren als Variablen zu speichern. Siehe Button.OnClick oder die ganzen anderen Events. Dieser wird dann als Eigenschaft (Property) nach außen geführt. Somit können andere Klassen darauf zugreifen.

In dem Execute wird jetzt mit Hilfe von Synchronize die Methode SyncBinFertig aufgerufen. Wir haben dort eine zusätzliche Methode weil Synchronize nur Proceduren ohne Parameter akzeptiert. Was macht Synchronize? Synchronize sorgt dafür, dass der VCL-Thread die Methode SyncBinFertig aufruft. Und wie ihr euch denken werdet ist somit der Kontext von unserem Thread auf den VCL-Thread umgeleitet worden. Synchronize macht aber noch etwas. Und zwar wartet es so lange bis die Methode SyncBinFertig zu Ende ausgeführt wurde. Das hat zu bedeuten, dass unser Thread für diesen Zeitraum stehen bleibt. Danach geht alles wie gewohnt weiter. Allerdings muss man darauf achten, dass der VCL-Thread zu diesem Zeitpunkt nichts zu tun hat. Das soll bedeuten sobald der VCL-Thread in einer Methode steckt (Button1OnClick) wird kein Synchronize abgearbeitet! Das kann zu sogenannten Deadlocks führen. Dazu aber unten mehr.

Warum müssen wir an dieser Stelle synchronisieren?
Wir müssen an dieser Stelle nicht immer synchronisieren. Wir müssen nur dann synchronisieren, wenn wir das Ergebnis in einen, vom einem Thread (VCL-Thread oder anderer) verwalteten Speicherbereich, schreiben wollen!

Hier einmal zwei Beispiele die das etwas veranschaulichen sollen.

Beispiel 1 in dem wir nicht synchronisieren müssen.

procedure Form1.BinFertig(const Content: String);
var
  FS: TFileStream;
begin
  FS := TFileStream.Create(FileName, fmCreate);
  FS.Write(Content[1], Length(Content));
  FS.Free;
end;

Beispiel 2 in dem wir synchronisieren müssen.

procedure Form1.BinFertig(const Content: String);
begin
  Form1.Buffer := Form1.Buffer + Content;
end;

Warum müssen wir Beispiel 2 synchronisieren und Beispiel 1 nicht?

Ihr erinnert euch bestimmt an das was ich oben gesagt hatte? Nein. Macht nichts. Und zwar sagt ich, dass Thread 1 mit seinen Berechnungen unterbrochen wird und Thread 2 etwas machen darf. So was ist wenn jetzt 2 von den 5 Threads gleichzeitig fertig geworden sind? Thread 1 ruft BinFertig auf. Diese Methode will nun den Content ihrer Seite an den bereits vorhandenen anhängen. Der Thread ist zu 50% mit der Arbeit fertig und nun ist Thread 2 an der Reihe! Auch er hängt jetzt ein bisschen was an den vorhandenen an. Jetzt die Frage was kommt im Endeffekt dabei raus? Ich weiß es auch nicht! Ich schätze aber mal, dass entweder totaler Datenmüll oder ein Absturz dabei raus kommt. Und genau aus diesem Grund muss hier synchronisiert werden. So kann nur maximal 1 Thread etwas an dem Buffer anhängen und die anderen müssen warten!

In Beispiel 1 muss nicht synchronisiert werden, da die Daten auf die Platte geschrieben werden. Es macht dort keinen Unterschieb ob der Thread unterbrochen wird oder nicht. VORSICHT! Wenn jetzt die gesamten Threads aber mit ein und dem selben FileStream arbeiten dann muss auch synchronisiert werden. Da dieser FileStream sozusagen ein und den selbe Speicherbereich darstellt!

Ein anderer Fall in dem synchronisiert werden muss ist, wenn ein Thread gerade etwas lesen möchte und ein anderer etwas genau dort hin schreiben will. In diesem fall kann genau das selbe passieren wie in Beispiel 2 wenn nicht synchronisiert wird.

Speicherbereiche schützen

Eine andere Möglichkeit etwas zu schütze sind sogenannte critical Sections (zu gut Deutsch: kritische Bereiche). Der Buffer in Beispiel 2 wäre ein solcher Bereich. Critical Sections ermöglichen es dem Entwickler einen Bereich gegen andere Threads zu sichern.

Also Erstes brauchen wir eine Membervariable in Form1. Diese muss das z.B.: im OnCreate erzeugt werden.

uses
  ..., SyncObjs;
 
  TForm1 = class(TForm)
    ...
  private
    FBufferCritSect: TCriticalSection;
    ...
  end;

Anschließend sieht unser BinFertig so aus.

procedure Form1.BinFertig(const Content: String);
begin
  FBufferCritSect.Enter;
  Form1.Buffer := Form1.Buffer + Content;
  FBufferCritSect.Leave;
end;

Nun brauchen wir unser BinFertig auch nicht mehr synchronisieren. Warum den das jetzt? Die CriticalSection die wir eingesetzt haben würde dafür sorgen, dass maximal 1 Thread sich in dem Block zwischen Enter und Leave aufhalten würde. Alle anderen müssten dann warten bis sie an der Reihe sind. Dadurch, dass wir nicht mehr synchronisieren müssen, würde der Kontext wieder bei den einzelnen Thread bleiben. Unser VCL-Thread würde also mit einer critical Section nicht zu tun haben. (er würde Däumchen drehen) Dafür könnte er sich aber voll und ganz um dem Rest kümmern können. Um zu vermeiden, dass bei einem Fehler die Critical Section für immer und ewig verschlossen bleibt (Deadlock) sollte man einen Ressourcenschutzblock um sie herum errichten. Das soll bedeuten. Auch im Fehlerfall würde eine gewisse Aktion IMMER ausgeführt werden. Unsere Exception wird dadurch aber nicht abgefangen! Sie wird ganz normal weitergeleitet.

procedure Form1.BinFertig(const Content: String);
begin
  FBufferCritSect.Enter;
  try
    Form1.Buffer := Form1.Buffer + Content;
  finally
    // Egal was auch passiert die Section würde immer verlassen werden
    // Sofern die Section und die Anwendung noch existieren, und
    // der Rechner noch Läuft. ;)
    FBufferCritSect.Leave;
  end;
end;

Was ist denn das schon wieder?

Was wäre das Leben wenn das bisher schon alle Probleme waren? Einfach langweilig ist weiß!

Also hier noch einmal ein nicht unbedingt triviales Problem. Was ist passiert wenn meine Anwendung auf einmal ohne ehrsichtlich Grund stehen bleibt? Also es tritt kein Fehler auf. Nein. Sie bleibt einfach stehe und ich kann nichts dagegen unternehmen.

Das was man sich dann eingefangen hat ist Deadlock. Das Programm wartet an 2 Stellen darauf, dass sich jeweils die andere fertig wird. Ein hoffentlich einfaches Beispiel. Wir haben einen Thread der besitzt eine Methode. In dieser Methode wartet er darauf, dass der Thread etwas ganz bestimmtes getan hat und kehrt dann zurück. Wenn diese Methode jetzt aus dem VCL-Thread aufgerufen und in diesem Thread, in der Zeit wären der VCL-Thread auf das rückkehren der Methode wartet, ein Synchronize aufgerufen wird, dann ergibt das einen Deadlock. Die Erklärung ist einfach. Das Synchronize wartete ja darauf, dass der VCL-Thread es abarbeiten kann. Wenn dieser aber selber wartet, dann bleiben beide stehen.

Ein anderes Beispiel haben wir im Abschnitt darüber kennen gelernt. Und zwar wenn durch einen Fehler die critical Section nicht wieder geöffnet wurde.

Prioritäten

Das Setzen von Threadprioritäten ist geschieht über die Eigenschaft Priority eines Threads. Was ist das Hinterlistige an den Prioritäten? Oder anders gesagt worauf sollte man achten, wenn man Prioritäten einstellt. Standardmäßig wird von jedem Thread oder Prozess die Priorität Normal verwendet. Wenn ich nun aber einen Thread habe der eine höhere Prio erhalten soll dann muss ich darauf achten, dass er noch genügend Ressourcen für die Anderen übrig lässt. Das soll mal an einem kleinen Beispiel erläutert werden. Wir haben einen Thread der besonders wichtige Berechnungen durchführt. Diese sollen sehr Zeitkritisch also mit hoher Prio abgearbeitet werden. In dem Execute muss ich also nachschauen ob ich eine Berechnung vorliegen habe.

while (not Terminated) do begin
  if (BerechnungDa) then begin
    // berechne
  end;
end;

An und für sich wäre das ja schon die Lösung des Problems. Allerdings dadurch, dass ich permanent nachschaue ob ich eine Berechnung da habe, würde dieser Thread so viel an CPU klauen, dass für die anderen nichts mehr übrig bliebe. Wenn ich die Prio auf Echtzeit gesetzt habe kann es sogar sein, dass Windows stehen bleibt und sich nur noch auf das konzentriert. Eine Lösung für dieses Problem wäre wenn ich nur ein paar mal in der Sekunde nachschauen würde.

while (not Terminated) do begin
  if (BerechnungDa) then begin
    // berechne
  end else begin
    sleep(10);
  end;
end;

Das würde dafür sorgen, dass nur 100mal in der Sekunde abgefragt werden würde. Diese Auflösung würde schon für die meisten Sachen vollkommen ausreichen aber wir können noch einen drauf setzen. Das hätte dann eine noch höhere Auflösung bei weniger Aufwand (Abfragen in der Sekunde) von unserer Seite. Das Ganze würde dann mit Hilfe eines Events arbeiten. Events sind in der Unit SyncObjs deklariert.

Event := TEvent.Create(nil, true, false, '');

So wird ein Event erzeugt. Der erste Parameter ist ein Pointer auf ein SecureAttribut. Das ist in den meisten Fällen (bei mir immer ;) ) nil. Viel interessante sind die anderen drei. Der zweite Parameter gibt an ob das Event sich von alleine wieder zurücksetzen kann. Der Vorteil davon, jedes Mal wenn dieses Event ausgelöst wird kann immer nur einer der darauf wartenden Parteien (Threads) das Signal bekommen. Der dritte ist der Initialisierungsstatus. Sprich ob es zu Begin gleich gesetzt ist oder lieber doch nicht. Der Vierte ist wieder sehr geil. Dieser ist der Name des Events. Warum zu Teufel brauch ein Event einen Namen? Ganz einfach, wenn es von mehren Anwendungen gleichzeitig abgefragt bzw. gesetzt werden soll dann muss es ja eindeutig identifizierbar sein. Und genau das macht der Name. Ihr erzeugt euch zwei Events mit dem Selben Namen und schon könnt ihr andere Anwendungen damit triggern. :) Aber wieder zurück zum Thema. So würde unsere Quelle mit einem Event ausschauen.

while (not Terminated) do begin
  if (Event.WaitFor(100) = wrSignaled) then begin
    // Sofern sich das Event nicht von alleine zurücksetzt müssen wir noch
    // Event.ResetEvent aufrufen.
    // Berechne was
  end;
end;

In der Methode die uns normalerweise die Berechnung übergeben würde müssen wir nur noch das Event auslösen (Event.SetEvent) und schon klappt alles. Was passiert bei WaitFor? WaitFor ist so konzipiert, dass es 100 Millisekunden warten würde und wenn das Event immer noch nicht ausgelöst wurde, dann würde es mit wrTimeOut zurückkommen. Wenn dieses Event jetzt allerdings nach 10 Millisekunden schon ausgelöst wurde, dann wartet WaitFor logischerweise nicht noch 90 Millisekunden. Nein. Es kehrt sofort wieder zurück und als Rückgabewert würden wir wrSignaled bekommen. Warum habe ich jetzt allerdings 100Millisekunden verwendet und nicht etwa unendlich (INFINITE) oder 15 Minuten? Na weil wir ja sonst nicht wüssten wann unser Thread terminiert wurde! So wird 10 mal in der Sekunde abgeprüft ob er terminiert wurde.

Schlusswort

Wenn man sich einen Thread erzeugen will der in einem Fenster OpenGL rendert, dann darf (sollte) man dort so gut wie gar nichts synchronisieren. Allerdings wenn man etwas synchronisiert, dann MUSS alles synchronisiert werden. Also ich denke jetzt an das Initialisieren, das Rendern und Löschen des Renderkontext. Und wenn man alles synchronisiert, dann kann man auch komplett auf den Thread verzichten. Weil dadurch alles im VCL-Thread ausgeführt wird. Und das ist ja eigentlich nicht Sinn und Zweck davon.

Also Merke! Nur das Notwendigste synchronisieren oder versuchen ganz auf den Thread zu verzichten. Und das Notwendigste ist alles das wo sich Überschneidungen beim Schreiben ergeben können. Lesen alleine (kein Schreiben) ist ungefährlich, weil dort ja keine Daten verändert werden.

In diesem Sinne.

Lossy eX