Tutorial Multithreading

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Multithreading

Vorwort

In diesem (meinem ersten) Tutorial versuche ich euch etwas in das Mysterium der Threads einzuweihen und ich werde versuchen euch die Leistungsfähigkeit und die Gefahren etwas näher zu bringen.

Was ist ein Thread?

Als erstes sollten wir einmal klären was ein Thread überhaupt ist.
Ein Thread ist die Möglichkeit mehrere Quellcodes zur "gleichen Zeit" ausführen zu können.
Hier haben wir schon Trugschluss Nummer eins! In echt werden diese Quellen nicht zur gleichen Zeit ausgeführt. Sie werden abwechselnd aufgerufen. Dies geschieht allerdings so schnell, dass man es mit bloßem Auge nicht sehen kann.

Nehmen wir mal an wir haben 2 Threads in einer Anwendung und beide führen große Berechnungen aus. Dann wird es so aussehen, dass Thread 1 anfängt etwas zu berechnen und dann (nach wenigen Millisekunden) wird er vom Betriebssystem unterbrochen und Thread 2 darf etwas rechnen. Dieser wird dann auch wieder unterbrochen und Thread 1 darf wieder. So geht das weiter bis nichts zu berechnen mehr übrig ist.

Neben dem Multithreading gibt es noch das sogenannt Multiprocessing.
Der Unterschied zum Threading ist der, dass wir mehrere Anwendungen haben und somit auch vollkommen unterschiedliche Speicherbereiche.
Das ist beim Multithreading nicht der Fall, da wir ja nur eine Anwendung haben. Und aus den gleichen Speicherbereichen und den Unterbrechungen resultiert die Notwendigkeit, dass wir die Threads untereinander synchronisieren müssen (siehe Abschnitt Synchronschwimmen).

Der Richtigkeit halber sollte hier erwähnt werden, dass zu Beginn einer jeden Anwendung schon ein eigenen Thread verfügbar ist (sonst würde sie ja nicht funktionieren). Dieser nennt sich VCL-Thread und arbeitet Windowsbotschaften (etc.) ab. So gesehen ist er die Mutter für alles. Von diesem Thread sieht der Entwickler so gut wie gar nichts. Aber es ist da!

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 Spiel programmiert in dem im Hintergrund schon ein neuer Level geladen wird. Das beste 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.
  2. wo 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 Webseite herunter laden.


Das waren allerdings nur 2 Beispiele von vielen. Es ist der Kreativität eines Entwicklers freien Lauf gelassen.
Desweiten ermöglichen es Threads auch mehrere im System vorhandene Prozessoren anzusprechen. Und somit zum Beispiel auf dem einen Prozessor zu Rendern und auf dem anderen die notwendigen Berechnungen durchzuführen.

Wie kann ich sie denn erstellen?

Das Erstellen eines Thread ist in Delphi wahnsinnig einfach. *eg*
Das Einzige was wir tun müssen ist die Klasse TThread abzuleiten und die Methode Execute zu überschreiben.
Wer keine Ahnung von Objekt orientiertem Programmieren (OOP) hat sollte sich an dieser Stelle erst einmal darüber schlau machen.

interface

uses
  classes;

type
  TMyOwnThread = class(TThread)
  protected
    procedure Execute; override;
  end;

implementation

procedure TMyOwnThread.Execute;
begin
  // Führe hier irgendwelche Berechnungen aus.
end;

Das war es.
Ihr werdet euch jetzt Fragen was daran so kompliziert ist!
Bisher noch nichts! Das Komplizierte kommt erst jetzt. Wenn ihr zum Beispiel in dem Execute einen Fehler gemacht habt (Was man nie ausschließen kann) oder ihr versucht eine Datei zu öffnen die aber nicht existiert. Dann wird von Delphi (zu Recht) eine Exception ausgelöst. Normalerweise wird diese von der Anwendung abgefangen und als Fehlermeldung (mit rotem Ausrufezeichen) ausgegeben. Das hat bestimmt schon jeder einmal gesehen. Aber in einem Thread haben wir ein anderes Verhalten. Hier wird dieser Fehler spätestens vom Betriebssystem abgefangen. Und das Betriebssystem terminiert zum Dank dann eure Anwendung mit dem Fehler "Unknow Software Error". Das tollste an der Sache ist aber. Sobald die Anwendung aus Delphi heraus gestartet wird fängt Delphi diesen Fehler ab. Allerdings liefert Delphi dann KEINEN Fehler. Es kommt nicht einmal eine Warnung! Es kommt gar nichts! Die Anwendung läuft ohne Fehler weiter. Und sobald sie außerhalb von Delphi gestartet wird, wird sie vom Betriebssystem abgeschossen.

Wie kann ich dem vorbeugen?
Und zwar in dem ich die Exceptions abfange. Auch wenn ich sie dann einfach nur ignoriere (was natürlich dreckig wäre) aber abfangen muss ich sie.

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 dasselbe wie jede andere Klasse auch. Sprich er kann genau so erweitert werden wie das Form1 oder sonst irgendeine 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:

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.
Info DGL.png 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 dem Laden anfängt. Wenn dieser fertig ist dann sagt er dem 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.

Warnung.png 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ützen 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 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 Priorität 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 Priorität 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 Priorität 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 ist ungefährlich, weil dort ja keine Daten verändert werden.

In diesem Sinne.

Euer
Lossy eX


Vorhergehendes Tutorial:
Tutorial Komponentenentwicklung
Nächstes Tutorial:
Tutorial Software-Synthesizer

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.