Difference between revisions of "Multithreaded Application Tutorial/de"

From Free Pascal wiki
Jump to navigationJump to search
m (Fixed syntax highlighting; removed categories included in template)
 
(57 intermediate revisions by 15 users not shown)
Line 1: Line 1:
 
{{Multithreaded Application Tutorial}}
 
{{Multithreaded Application Tutorial}}
 
+
<br>
(Seite befindet sich in Übersetzung...)
+
Zurück zu den [[Additional information/de|Zusätzlichen Informationen]].<br>
 
+
<br>
= Überblick =
+
== Überblick ==
  
 
Diese Seite soll zeigen, wie man unter FreePascal und Lazarus Multithread-Anwendungen erstellt und verwaltet. In einer Multithread-Anwendung lassen sich verschiedene Aufgaben auf mehrere Threads verteilen, die gleichzeitig ausgeführt werden können.  
 
Diese Seite soll zeigen, wie man unter FreePascal und Lazarus Multithread-Anwendungen erstellt und verwaltet. In einer Multithread-Anwendung lassen sich verschiedene Aufgaben auf mehrere Threads verteilen, die gleichzeitig ausgeführt werden können.  
  
Wenn Sie bisher keinerlei Erfahrungen zur Multithread-Programmierung gemacht haben, empfehlen wie Ihnen, sich zunächst den Artikel "Benötigt Ihre Anwendung wirklich Multithread-Eigenschaften?" sorgfältig durchzulesen, da Multithread-Programmierung kein leichtes Unterfangen ist.
+
Wenn Sie bisher keinerlei Erfahrungen zur Multithread-Programmierung gemacht haben, empfehlen wie Ihnen, sich zunächst den Artikel "Benötigt Ihre Anwendung wirklich Multithread-Eigenschaften?" sorgfältig durch zu lesen, da Multithread-Programmierung kein leichtes Unterfangen ist.
  
 
Das Hauptziel der Multithread-Programmierung ist die Verfügbarkeit der Benutzeroberfläche eines Programms, während es im Hintergrund Berechnungen durchführt. Dies kann man erreichen, indem man die Berechnung in einen Thread außerhalb des sogenannten Main-Thread verlagert, welcher für die Aktualisierung der Benutzeroberfläche zuständig ist.
 
Das Hauptziel der Multithread-Programmierung ist die Verfügbarkeit der Benutzeroberfläche eines Programms, während es im Hintergrund Berechnungen durchführt. Dies kann man erreichen, indem man die Berechnung in einen Thread außerhalb des sogenannten Main-Thread verlagert, welcher für die Aktualisierung der Benutzeroberfläche zuständig ist.
Line 17: Line 17:
 
Wichtig: Der Main Thread wird beim Start Ihrer Anwendung vom Betriebssystem erstellt. Der Main Thread ist dabei der einzige Thread (und '''muss''' auch der einzige bleiben), der für die Aktualisierung der Komponenten der Benutzeroberfläche zuständig ist (Forms, etc.) (ansonsten hängt sich Ihre Anwendung auf).
 
Wichtig: Der Main Thread wird beim Start Ihrer Anwendung vom Betriebssystem erstellt. Der Main Thread ist dabei der einzige Thread (und '''muss''' auch der einzige bleiben), der für die Aktualisierung der Komponenten der Benutzeroberfläche zuständig ist (Forms, etc.) (ansonsten hängt sich Ihre Anwendung auf).
  
= Benötigt Ihre Anwendung wirklich multithread-Eigenschaften? =
+
== Benötigt Ihre Anwendung wirklich Multithread-Eigenschaften? ==
  
Wenn Sie neu in der Multithread programmierung sind und sie lediglich eine bessere reaktionsfähigkeit ihrer Anwendung wärend langer Berechnungen verbessern wollen, dann muss nicht unbedingt Multithreading die einfachste lösung sein.
+
Wenn Multithread Programmierung für Sie Neuland ist und Sie lediglich eine bessere Reaktionsfähigkeit ihrer Anwendung während langer Berechnungen benötigen, dann ist Multithreading nicht unbedingt die einfachste Lösung.
Multithreading Applikationen sind immer schwiriger zu debuggen und auch oft viel komplexer. Ausserdem benötigen Sie in vielen Fällen kein Multithreading um ihre Anwendung reaktionsfähig zu halten. Stattdessen können Sie ein '''Application.ProcessMessages''' in ihre Berechnungen mit einbauen dieses verarbeitet alle anstehenden Narichten die an ihre Applikation gesendet wurden und Ihre Applikation reagiert auf Ereignisse. Sie können also einen Teil der Brechnung durchführen dann Application.ProcessMessages aufrufen und die Benutzereingaben werden verarbeitet und die Oberfläche neu gezeichent. Platzieren sie das Application.Processmessages z.b. in Schleifen so das bei jedem Schleifendurchlauf die benutzeroberfläche reagiert.  
+
Multithreading Applikationen sind immer schwieriger zu debuggen und auch oft viel komplexer. Außerdem benötigen Sie in vielen Fällen kein Multithreading, um ihre Anwendung reaktionsfähig zu halten. Statt dessen können Sie ein '''Application.ProcessMessages''' in Ihre Berechnungen mit einbauen. Dieses verarbeitet alle anstehenden Nachrichten, die an Ihre Applikation gesendet wurden und Ihre Applikation reagiert auf Ereignisse. Sie können also einen Teil der Berechnung durchführen, dann Application.ProcessMessages aufrufen und die Benutzereingaben werden verarbeitet und die Oberfläche neu gezeichnet. Platzieren Sie das Application.Processmessages z.B. in Schleifen, so dass bei jedem Schleifendurchlauf die Benutzeroberfläche reagiert.  
  
Zum Beispiel: In den Lazarus Beispielen unter  
+
Zum Beispiel: In den Lazarus-Beispielen unter  
 
examples/multithreading/singlethreadingexample1.lpi
 
examples/multithreading/singlethreadingexample1.lpi
wird eine grosse Datei gelesen und verarbeitet und die oben genannte Technik benutzt.
+
wird eine große Datei gelesen und verarbeitet und die oben genannte Technik benutzt.
  
 
Multithreading wird wirklich benötigt für:
 
Multithreading wird wirklich benötigt für:
* blocking handles, like network communications
+
* blocking handles, wie Netzwerkkommunikation, Interrupts (Linux)
 
* Mehrprozessor- oder Mehrkernbetrieb
 
* Mehrprozessor- oder Mehrkernbetrieb
* Algorythmen und Bibliotheksaufrufe, die nicht in mehrere kleine Stufen zerteilt werden können.
+
* Algorithmen und Bibliotheksaufrufe, die nicht in mehrere kleine Stufen zerteilt werden können.
  
= Die Klasse TThread =
+
== Die Klasse TThread ==
  
 
Das folgende Beispiel ist im Verzeichnis examples/multithreading/ zu finden.
 
Das folgende Beispiel ist im Verzeichnis examples/multithreading/ zu finden.
 
 
Der einfachste Weg, um eine Multithread-Anwendung zu erstellen, ist die Verwendung der Klasse TThread.
 
Der einfachste Weg, um eine Multithread-Anwendung zu erstellen, ist die Verwendung der Klasse TThread.
 
 
Über diese Klasse lässt sich, neben dem Main-Thread, ein zusätzlicher Thread in einfacher Weise erstellen.
 
Über diese Klasse lässt sich, neben dem Main-Thread, ein zusätzlicher Thread in einfacher Weise erstellen.
 +
Unter normalen Umständen müssen dazu bloß zwei Methoden überschrieben (override) werden: Der Konstruktor Create und die Methode Execute.
 +
Im Konstruktor Create wird der Thread für die spätere Ausführung vorbereitet. Hier werden die dem Thread zugrunde liegenden Variablen initialisiert.
 +
Der originale Konstruktor des Threads enthält einen Parameter namens "Suspended": Damit der Thread nicht automatisch nach seiner Erstellung gestartet wird, ist es empfehlenswert, diesen Parameter auf "true" zu setzen. In diesem Fall können Sie den Thread zu einem späteren Zeitpunkt über die Methode "Resume" starten.
 +
Ist es dagegen erwünscht, dass der Thread direkt nach seiner Erstellung startet, setzen Sie Suspended auf "false".
  
Unter normalen Umständen müssen dazu bloß zwei Methoden überschrieben (override) werden: Der Constructor Create und die Methode Execute.
+
Ab der Version 2.0.1 des FreePascal-Compilers lässt sich auch die Größe des von einem Thread verwendeten Stacks über einen Parameter des Konstruktor TThread.Create einstellen. Das kann beispielsweise bei rekursiven Prozeduren oder Funktionen nützlich sein.
 +
Wird die Größe des Stack nicht explizit festgelegt, verwendet FPC die vom Betriebssystem festgelegte Standardgröße.
  
Im Constructor Create wird der Thread für die spätere Ausführung vorbereitet. Hier werden die dem Thread zugrunde liegenden Variablen initialisiert.
+
In die (mit Parameter Override) überschriebene Methode Execute schreibt man den vom Thread auszuführenden Quelltext hinein.
Der originale Constructor des Threads benötigt einen "Suspended" genannten Parameter: Damit der Thread nicht automatisch nach seiner Erstellung gestartet wird, ist es empfehlenswert, diesen Parameter auf "true" zusetzen. In diesem Fall können Sie den Thread zu einem späteren Zeitpunkt über die Methode "Resume" starten.
 
Ist es dagegen erwünscht, dass der Thread direkt nach seiner Erstellung startet, setzen Sie Suspended auf false.
 
  
Ab der Version 2.0.1 des FreePascal-Compilers lässt sich auch die Größe des von einem Thread verwendeten Stacks über einen Parameter des Constructor TThread.Create einstellen.
+
Die Klasse TThread besitzt die wichtige Eigenschaft (property):
 
 
Deep procedure call recursions in a thread are a good example. If you don't specify the stack size parameter, a default OS stack size is used.
 
 
 
In the overrided Execute method you will write the code that will run on the thread.
 
 
 
The TThread class has one important property:  
 
 
Terminated : boolean;
 
Terminated : boolean;
 +
(Standardeinstellung: Terminated=false)
  
If the thread has a loop (and this is usual), the loop should be exited when Terminated is true (it is false by default). So in each cycle, it must check if Terminated is True, and if it is, must exit the .Execute method as quickly as possible, after any necessary cleanup.
+
Der Sinn der Eigenschaft Terminated ist, dass sich ein Thread damit zu einem beliebigen Zeitpunkt abbrechen lässt. Jedoch muss dies von dem Programmierer in den Quelltext der Methode Execute eingearbeitet werden - die Methode Terminate greift also nicht direkt in den vom Programmierer vorgesehenen Programmablauf ein.
 +
Sollten sich in Ihrem Thread also Repeatschleifen oder andere sich wiederholende Stellen finden, ist es deshalb notwendig, dass Sie in jeder Schleife explizit prüfen, ob die Eigenschaft Terminated wahr oder falsch ist. Sollte Terminated=true sein, muss die Methode Execute (evtl. nach einer finalen Konfiguration) auf schnellstem Wege verlassen werden.  
  
So keep in mind that the Terminate method does not do anything by default: the .Execute method must explicitly implement support for it to quit it's job.
+
Wie eingangs erwähnt, sollte der erstellte Thread in keinem Fall mit den sichtbaren Komponenten der Benutzeroberfläche agieren. Die Benutzeroberfläche verändern, Eingaben abfragen, etc. darf ausschließlich der Main-Thread.
  
As we explained earlier, the thread should not interact with the visible components. To show something to the user it must do so in the main thread.  
+
Um trotzdem aus einem Thread heraus auf die Benutzeroberfläche bzw. Variablen des Hauptprogrammes zuzugreifen, existiert die Methode Synchronize.
To do this, a TThread method called Synchronize exists.
+
Synchronize wird mit einer procedure als Parameter aufgerufen.  
Synchronize requires a method (that takes no parameters) as an argument.  
+
Wenn Sie Synchronize(@MyMethod) aufrufen, wird der Thread gestoppt, der Code in MyMethod wird im Hauptthread ausgeführt, und dann die Bearbeitung des Threads fortgesetzt. Die exakte Arbeitsweise von Synchronize hängt von der Plattform ab.
When you call that method through Synchronize(@MyMethod), the thread execution will be paused, the code of MyMethod will run in the main thread, and then the thread execution will be resumed. The exact working of Synchronize depends on the platform, but basically it does this: It posts a message onto the main message queue and goes to sleep. Eventually the main thread processes the message and calls MyMethod. This way MyMethod is called without context, that means not during a mouse down event or during paint event, but after. After the main thread executed MyMethod, it wakes the sleeping Thread and processes the next message. The Thread then continues.
 
  
There is another important property of TThread: FreeOnTerminate. If this property is true, the thread object is automatically freed when the thread execution (.Execute method) stops. Otherwise the application will need to free it manually.
+
Eine andere wichtige Eigenschaft (property) von TThread ist FreeOnTerminate. Wenn diese Eigenschaft auf True gesetzt ist, wird der Thread automatisch freigegeben (wie ein Aufruf von .Free), wenn Execute beendet wird.  
 
+
Beispiel:
Example:
 
  
 +
<syntaxhighlight lang=pascal>
 
   Type
 
   Type
 
     TMyThread = class(TThread)
 
     TMyThread = class(TThread)
Line 75: Line 71:
 
       procedure Execute; override;
 
       procedure Execute; override;
 
     public
 
     public
       Constructor Create(Suspended : boolean);
+
       Constructor Create(CreateSuspended : boolean);
 
     end;
 
     end;
  
Line 85: Line 81:
  
 
   procedure TMyThread.ShowStatus;
 
   procedure TMyThread.ShowStatus;
   // this method is executed by the mainthread and can therefore access all GUI elements.
+
   // Diese Methode wird vom MainThread ausgeführt und kann deshalb auf alle GUI-Elemente zugreifen
 
   begin
 
   begin
 
     Form1.Caption := fStatusText;
 
     Form1.Caption := fStatusText;
Line 100: Line 96:
 
       begin
 
       begin
 
         ...
 
         ...
         [here goes the code of the main thread loop]
+
         // here goes the code of the main thread loop
 
         ...
 
         ...
 
         if NewStatus <> fStatusText then
 
         if NewStatus <> fStatusText then
Line 109: Line 105:
 
       end;
 
       end;
 
   end;
 
   end;
 +
</syntaxhighlight>
  
On the application,
+
In der Hauptunit erfolgt dann folgender Aufruf
  
 +
<syntaxhighlight lang=pascal>
 
   var
 
   var
 
     MyThread : TMyThread;
 
     MyThread : TMyThread;
 
   begin
 
   begin
     MyThread := TMyThread.Create(True); // This way it doesn't start automatically
+
     MyThread := TMyThread.Create(True); // So startet es nicht automatisch
 
     ...
 
     ...
     [Here the code initialises anything required before the threads starts executing]
+
     // Here the code initialises anything required before the threads starts executing
 
     ...
 
     ...
 
     MyThread.Resume;
 
     MyThread.Resume;
 +
  end;
 +
</syntaxhighlight>
 +
 +
Soll Ihr Programm flexibler sein, können Sie ein Ereignis für den Thread erzeugen - Ihre synchronisierte Methode ist dann nicht eng an eine bestimmte Form oder Klasse gebunden und Listeners können mit dem Ereignis des Threads verbunden werden. Hier ein Beispiel:
 +
 +
<syntaxhighlight lang=pascal>
 +
  Type
 +
    TShowStatusEvent = procedure(Status: String) of Object;
 +
 +
    TMyThread = class(TThread)
 +
    private
 +
      fStatusText : string;
 +
      FOnShowStatus: TShowStatusEvent;
 +
      procedure ShowStatus;
 +
    protected
 +
      procedure Execute; override;
 +
    public
 +
      Constructor Create(CreateSuspended : boolean);
 +
      property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
 +
    end;
 +
 +
  constructor TMyThread.Create(CreateSuspended : boolean);
 +
  begin
 +
    FreeOnTerminate := True;
 +
    inherited Create(CreateSuspended);
 
   end;
 
   end;
  
= Special things to take care of =
+
  procedure TMyThread.ShowStatus;
There is a potential headache in Windows with Threads if you use the -Ct (stack check) switch.
+
  // Diese Methode wird vom MainThread ausgeführt und kann deshalb auf alle GUI-Elemente zugreifen
For reasons not so clear the stack check will "trigger" on any TThread.Create if you use the default stack size.
+
  begin
The only work-around for the moment is to simply not use -Ct switch. Note that it does NOT cause an exception in
+
    if Assigned(FOnShowStatus) then
the main thread, but in the newly created one. This "looks" like if the thread was never started.
+
    begin
 +
      FOnShowStatus(fStatusText);
 +
    end;
 +
  end;
  
A good code to check for this and other exceptions which can occur in thread creation is:
+
  procedure TMyThread.Execute;
 +
  var
 +
    newStatus : string;
 +
  begin
 +
    fStatusText := 'TMyThread Starting...';
 +
    Synchronize(@Showstatus);
 +
    fStatusText := 'TMyThread Running...';
 +
    while (not Terminated) and ([any condition required]) do
 +
      begin
 +
        ...
 +
        [here goes the code of the main thread loop]
 +
        ...
 +
        if NewStatus <> fStatusText then
 +
          begin
 +
            fStatusText := newStatus;
 +
            Synchronize(@Showstatus);
 +
          end;
 +
      end;
 +
  end;
  
 +
</syntaxhighlight>
  
     MyThread:=TThread.Create(False);
+
Und im Hauptformular
 +
 
 +
<syntaxhighlight lang=pascal>
 +
  Type
 +
    TForm1 = class(TForm)
 +
      Button1: TButton;
 +
      Label1: TLabel;
 +
      procedure FormCreate(Sender: TObject);
 +
      procedure FormDestroy(Sender: TObject);
 +
    private
 +
      { private declarations }
 +
      MyThread: TMyThread;
 +
      procedure ShowStatus(Status: string);
 +
    public
 +
      { public declarations }
 +
    end;
 +
 
 +
  procedure TForm1.FormCreate(Sender: TObject);
 +
  begin
 +
    inherited;
 +
    MyThread := TMyThread.Create(true);
 +
    MyThread.OnShowStatus := @ShowStatus;
 +
  end;
 +
 
 +
  procedure TForm1.FormDestroy(Sender: TObject);
 +
  begin
 +
    MyThread.Terminate;
 +
    MyThread.Free;
 +
    inherited;
 +
  end;
 +
 
 +
  procedure TForm1.Button1Click(Sender: TObject);
 +
  begin
 +
  MyThread.Resume;
 +
  end;
 +
 
 +
  procedure TForm1.ShowStatus(Status: string);
 +
  begin
 +
    Label1.Caption := Status;
 +
  end;
 +
</syntaxhighlight>
 +
 
 +
== Worauf Sie achten sollten ==
 +
Wenn Sie den Compilerswitch -Ct (Stack Check) benutzen, kann das einige Probleme mit sich bringen (Originalübersetzung: Es könnte Ihnen Kopfschmerzen bereiten :))
 +
Aus unbekannten Gründen "triggert" der Stack Check an jedem TThread.Create, wenn Sie die Standardgröße des Stacks benutzen.
 +
Die einzige Lösung ist zur Zeit, -Ct nicht zu benutzen.
 +
 
 +
Um auf Exceptions im Thread zu prüfen, können Sie folgendermaßen vorgehen:
 +
 
 +
<syntaxhighlight lang=pascal>
 +
     MyThread := TThread.Create(False);
 
     if Assigned(MyThread.FatalException) then
 
     if Assigned(MyThread.FatalException) then
 
       raise MyThread.FatalException;
 
       raise MyThread.FatalException;
 +
</syntaxhighlight>
  
 +
Dieser Code sorgt dafür daß Exceptions, die während der Erzeugung des Threads auftreten, im Main Thread einen Ausnahmefehler auslösen.
  
This code will asure that any exception which occured during thread creation will be raised in your main thread.
+
== Für Multithread-Anwendungen benötigte Units ==
  
= Für Multithread-Anwendungen benötigte Units =
+
Unter Windows benötigen Sie keine spezielle Unit, damit es funktioniert.
Unter Windows benötigen sie keine spezielle Unit, damit es funktioniert.
+
Unter Linux, MacOSX und FreeBSD benötigen Sie die Unit cthreads und diese ''muss'' die erste verwendete Unit im Projekt sein (die Programm-Unit, .lpr)!
Unter Linux, MacOSX und FreeBSD benötigen sie die cthreads Unit und diese ''muss'' die erste verwendete Unit im Projekt sein (die Programm-Unit, .lpr)!
 
  
Daher sollte der Code ihrer Lazarus Anwendung etwa so aussehen:
+
Daher sollte der Code Ihrer Lazarus-Anwendung etwa so aussehen:
  
 +
<syntaxhighlight lang=pascal>
 
   program MyMultiThreadedProgram;
 
   program MyMultiThreadedProgram;
 
   {$mode objfpc}{$H+}
 
   {$mode objfpc}{$H+}
Line 152: Line 249:
 
     Interfaces, // dies bindet das LCL Widgetset ein
 
     Interfaces, // dies bindet das LCL Widgetset ein
 
     Forms
 
     Forms
     { fügen sie ihre Units hier hinzu },
+
     { fügen Sie Ihre Units hier hinzu },
 +
</syntaxhighlight>
  
If you forget this you will get this error on startup:
+
Wenn Sie dies vergessen, kommt die folgende Fehlermeldung:
 
   This binary has no thread support compiled in.
 
   This binary has no thread support compiled in.
 
   Recompile the application with a thread-driver in the program uses clause before other units using thread.
 
   Recompile the application with a thread-driver in the program uses clause before other units using thread.
  
= SMP Unterstützung =
+
Lazarus Packages, die Multithreading beutzen, sollten '''-dUseCThreads''' zu den benutzerdefinierten Einstellungen für abhängige Pakete hinzufügen. Dazu öffnen Sie den Package-Editor für das entsprechende Paket, unter Einstellungen > Bedienung > Benutzerdefiniert fügen Sie ''-dUseCThreads'' hinzu. Dies wird diesen Compiler-Switch für alle Projekte und andere Packages, die dieses Package benutzen (die IDE eingeschlossen), definieren. Sowohl die IDE als auch alle von ihr neu erstellten Projekte, haben bereits folgenden Code in ihrer Projektdatei (.lpr):
The good news is that if your application works properly multithreaded this way, it is already SMP enabled!
+
 
 +
<syntaxhighlight lang=pascal>
 +
  uses
 +
    {$IFDEF UNIX}{$IFDEF UseCThreads}
 +
    cthreads,
 +
    {$ENDIF}{$ENDIF}
 +
</syntaxhighlight>
 +
 
 +
== SMP (Mehrprozessor) Unterstützung ==
  
= Debuging Multithread-Anwendungen mit Lazarus =
+
Sobald Sie Threads benutzen, werden diese vom Betriebsystem auf mehrere Prozessoren verteilt.
The debugging on Lazarus is not fully functional yet.  
 
  
== Debugger Ausgabe ==
+
== Debuging Multithread-Anwendungen mit Lazarus ==
  
In a single threaded application, you can simply write to console/terminal/whatever and the order of the lines is the same as they were written.
+
Das Debuggen von Multithread-Anwendungen wird von Lazarus zur Zeit noch nicht vollständig unterstützt.
In multithreaded application things are more complicated. If two threads are writing, say a line is written by thread A before a line by thread B, then the lines are not neccessarily written in that order. It can even happen, that a thread writes its output, while the other thread is writing a line.
 
  
The LCLProc unit contains several functions, to let each thread write to its own log file:
+
=== Debugger Ausgabe ===
 +
 
 +
In einer Anwendung mit einem Thread (dem Hauptthread), können Sie einfach auf die Konsole in ein Terminal oder ähnliches schreiben und die Reihenfolge der Zeilen wird chronologisch so angeordnet sein, wie Sie es ausgegeben haben.
 +
In Multithreaded Anwendungen wird dies etwas komplizierter. Wenn 2 Threads schreiben, sagen wir, eine Zeile wird von Thread A und eine von Thread B geschrieben, dann ist nicht gesagt, dass sie zeitlich in der richtigen Reihenfolge erscheinen, bzw. es kann vorkommen, dass ein Thread dem anderen in die Ausgabe hineinschreibt.
 +
 
 +
Die Unit LCLProc enthält einige Funktionen, um jeden Thread seine eigene Logdatei schreiben zu lassen:
 +
 
 +
<syntaxhighlight lang=pascal>
 
   procedure DbgOutThreadLog(const Msg: string); overload;
 
   procedure DbgOutThreadLog(const Msg: string); overload;
 
   procedure DebuglnThreadLog(const Msg: string); overload;
 
   procedure DebuglnThreadLog(const Msg: string); overload;
 
   procedure DebuglnThreadLog(Args: array of const); overload;
 
   procedure DebuglnThreadLog(Args: array of const); overload;
 
   procedure DebuglnThreadLog; overload;
 
   procedure DebuglnThreadLog; overload;
 +
</syntaxhighlight>
 +
 +
Zum Beispiel:
 +
Anstelle von ''writeln('irgendein Text ', 123);'' verwenden Sie
  
For example:
+
<syntaxhighlight lang=pascal>
Instead of ''writeln('Some text ',123);'' use
+
   DebuglnThreadLog(['irgendein Text ', 123]);
   DebuglnThreadLog(['Some text ',123]);
+
</syntaxhighlight>
  
This will append a line 'Some text 123' to '''Log<PID>.txt''', where <PID> is the process ID of the current thread.
+
Dies wird eine Zeile 'irgendein Text 123' an die '''Log<PID>.txt''' Datei anhängen, wobei <PID> die Prozess ID des aktuellen Threads ist.
  
It is a good idea to remove the log files before each run:
+
Es ist eine gute Idee, die Logdateien vor jedem Lauf zu entfernen:
 +
<syntaxhighlight lang="bash">
 
   rm -f Log* && ./project1
 
   rm -f Log* && ./project1
 +
</syntaxhighlight>
 +
 +
=== Linux ===
 +
Das Debuggen von grafischen Multithreading-Anwendungen unter Linux funktioniert mittlerweile problemlos. In der Vergangenheit traten aber aufgrund von Kompatibilitätsproblemen dabei Probleme mit dem X-Server auf: Er stürzte ab.
  
== Linux ==
+
Sollten bei Ihnen also solche Probleme auftreten, können Sie diese durch Starten einer neuen X-Instanz umgehen. Wie Sie dies erreichen, ist im Folgenden dargestellt. Eine andere Lösung ist bisher nicht bekannt.
Wenn sie versuchen, eine Multithread-Anwendung unter Linux zu debuggen, haben sie ein großes Problem: der X Server wird sich aufhängen.
 
  
Es ist nicht bekannt, wie man das richtig löst, aber eine Abhilfe ist:
+
==== X-Server in einem neuen Terminal starten ====
  
Erzeugen sie eine neue Instanz von X mit:
+
Starten Sie eine neue X-Instanz. Dies können Sie über die Konsole mit folgendem Befehl erreichen:  
  
 
   X :1 &
 
   X :1 &
  
It will open, and when you switch to another desktop (the one you are working with pressing CTRL+ALT+F7), you will be able to go back to the new graphical desktop with CTRL+ALT+F8 (if this combination does not work, try with CTRL+ALT+F2... this one worked on Slackware).
+
Nachdem Sie diesen Befehl ausgeführt haben, öffnet sich die neue X-Instanz. Mit den Tastenkombinationen [Strg]+[Alt]+[F7] und [Strg]+[Alt]+[F8] können Sie nun zwischen der Arbeitsoberfläche, auf der Sie arbeiten, und der neuen Instanz hin und her wechseln. (Bei der Slackware-Distribution und wenigen anderen wird dies durch die Kombination [Strg]+[Alt]+[F2] erreicht)
  
Then you could, if you want, create a desktop session on the X started with:
+
Als nächsten Schritt müssen Sie eine Desktop-Sitzung in der neuen X-Instanz starten. Starten Sie zum Beispiel eine neue gnome-Sitzung in der Konsole:  
  
 
   gnome-session --display=:1 &
 
   gnome-session --display=:1 &
  
Then, in Lazarus, on the run parameters dialog for the project, check "Use display" and enter :1.
+
==== X-Server in einem Fenster starten ====
 +
 
 +
Mit Xnest oder Xephyr können Sie auch einen X-Server in einem Fenster eines anderen X-Servers betreiben. Der Vorteil ist, dass Sie beim Debuggen nicht mehr zwischen verschiedenen Terminals wechseln müssen.
 +
 
 +
Mit dem Befehl
 +
 
 +
  Xnest :1 -ac
 +
 
 +
starten Sie mit Xnest eine X-Sitzung auf :1 und deaktivieren die Zugriffskontrolle.
 +
 
 +
Unter GNOME können Sie auch folgenden Befehl verwenden:
 +
 
 +
  gdmflexiserver --xnest
 +
 
 +
Dies startet nicht nur eine X-Sitzung; es wird auch gleichzeitig eine neue GDM-Sitzung, in der Sie sich anmelden können, gestartet.
 +
 
 +
==== Benötigte Einstellungen in Lazarus ====
 +
 
 +
Schließlich müssen Sie eine letzte Einstellung in Lazarus selbst vornehmen:
 +
Gehen Sie in der IDE in das Menü "Start"->"Start-Parameter...". Aktivieren Sie dort das Häkchen "Display verwenden", und tragen Sie in das Feld darunter die Zuordnungsnummer ":1" ein.
 +
 
 +
 
 +
Jetzt wird die Anwendung auf dem zweiten X Server laufen und Sie sind in der Lage, sie vom ersten aus zu debuggen.
  
Now the application will run on the seccond X server and you will be able to debug it on the first one.
+
Dies wurde mit Free Pascal 2.0 und Lazarus 0.9.10 unter Windows und Linux getestet.
  
This was tested with Free Pascal 2.0 and Lazarus 0.9.10 on Windows and Linux.
+
== Widgetsets ==
  
= Widgetsets =
+
Die win32, gtk und carbon Schnittstellen unterstützen Multithreading vollständig. Das bedeutet, TThread, critical sections und Synchronize funktioniert.
  
Die win32, gtk und carbon Schnittstellen unterstützen multithreading vollständig. This means, TThread, critical sections and Synchronize work.
+
== Critical sections ==
  
= Critical sections =
+
Eine ''critical section'' (kritischer Abschnitt) stellt sicher, dass in einer multi-thread Anwendung nicht gleichzeitig von verschiedenen Threads aus auf einen bestimmten Bereich des Codes zugegriffen wird. Bevor eine critical section benutzt werden kann, muss sie erstellt und initialisiert werden; wenn sie nicht mehr gebraucht wird, muss sie freigegeben werden.
  
A ''critical section'' is an object used to make sure, that some part of the code is executed only by one thread at a time.
+
Zur Verwendung von critical sections gehen Sie wie folgt vor:
A critical section needs to be created/initialized before it can be used and be freed when it is not needed anymore.
 
  
Critical sections are normally used this way:
+
Fügen Sie über ''uses'' die Unit '''SyncObjs''' hinzu, die die notwendigen Routinen beinhaltet.
  
Add the unit SyncObjs.
+
Als nächsten Schritt deklarieren Sie die critical section (für alle Threads, die auf die section Zugriff haben sollen):
  
Declare the section (globally for all threads which should access the section):
+
<syntaxhighlight lang=pascal>
 
   MyCriticalSection: TRTLCriticalSection;
 
   MyCriticalSection: TRTLCriticalSection;
 +
</syntaxhighlight>
  
Create the section:
+
Erstellen Sie die Section mit folgender Prozedur:
 
   InitializeCriticalSection(MyCriticalSection);
 
   InitializeCriticalSection(MyCriticalSection);
  
Run some threads. Doing something exclusively
+
 
 +
Starten Sie einige Threads, die exklusive Aufgaben durchführen:
 +
 
 +
<syntaxhighlight lang=pascal>
 
   EnterCriticalSection(MyCriticalSection);
 
   EnterCriticalSection(MyCriticalSection);
 
   try
 
   try
     // access some variables, write files, send some network packets, etc
+
     // An dieser Stelle können Sie auf Variablen zugreifen, Dateien schreiben, auf das Netzwerk zugreifen, u.s.w.
 
   finally
 
   finally
 
     LeaveCriticalSection(MyCriticalSection);
 
     LeaveCriticalSection(MyCriticalSection);
 
   end;
 
   end;
 +
</syntaxhighlight>
 +
 +
Nachdem alle Threads, die Zugriff auf die critical section hatten, beendet wurden, müssen Sie sie nun deinitialisieren:
  
After all threads terminated, free it:
+
<syntaxhighlight lang=pascal>
 
   DeleteCriticalSection(MyCriticalSection);
 
   DeleteCriticalSection(MyCriticalSection);
 +
</syntaxhighlight>
 +
 +
Alternativ können Sie die Klasse TCriticalSection benutzen. Das Erstellen eines Objektes der Klasse entspricht hier der Initialisierung, die Methode Enter entspricht obigem EnterCriticalSection, die Methode Leave entspricht der obigen LeaveCriticalSection und die Vernichtung (destruction) des Objektes erledigt das Freigeben.
  
As an alternative, you can use a TCriticalSection object. The creation does the initialization, the Enter method does the EnterCriticalSection, the Leave method does the LeaveCriticalSection and the destruction of the object does the deletion.
+
Beispiel: 5 Threads, die einen einzigen Zähler erhöhen:
 +
lazarus/examples/multithreading/criticalsectionexample1.lpi
  
For example: 5 threads incrementing a counter.
+
'''ACHTUNG:''' Sowohl die RTL als auch die LCL beinhalten beide obige 4 Funktionen. (Die Funktionen der LCL sind in den Units LCLIntf und LCLType definiert.) Beide erfüllen den gleichen Zweck. Sie können sowohl die Funktionen der RTL als auch die Funktionen der LCL zur selben Zeit in Ihrem Programm einsetzen. Sie sollten es nur vermeiden, die "critical section"-Funktionen der LCL auf die Funktionen der RTL anzuwenden und umgekehrt.
See lazarus/examples/multithreading/criticalsectionexample1.lpi
 
  
'''BEWARE:''' There are two sets of the above 4 functions. The RTL and the LCL ones. The LCL ones are defined in the unit LCLIntf and LCLType. Both work pretty much the same. You can use both at the same time in your application, but you should not use a RTL function with an LCL Critical Section and vice versus.
 
  
 +
=== Gemeinsamer Variablenzugriff ===
  
== Sharing Variables ==
+
Mehrere Threads können sich eine Variable im Lesezugriff teilen, ohne dass Probleme zu erwarten sind. Nur wenn der eine oder andere Thread den Wert der Variablen ändern möchte, muss sicher gestellt werden, dass niemals mehrere Threads gleichzeitig auf die Variable zugreifen.
  
If some threads share a variable, that is read only, then there is nothing to worry about. Just read it.
 
But if one or several threads changes the variable, then you must make sure, that only one thread accesses the variables at a time.
 
  
For example: 5 threads incrementing a counter.
+
Als Beispiel: 5 Threads inkrementieren einen Counter:
See lazarus/examples/multithreading/criticalsectionexample1.lpi
+
lazarus/examples/multithreading/criticalsectionexample1.lpi
  
= Waiting for another thread =
+
== Wenn ein Thread das Ergebnis eines anderen benötigt... ==
  
If a thread A needs a result of another thread B, it must wait, till B has finished.  
+
Wenn ein Thread A das Ergebnis eines Threads B benötigt, muss er warten, bis B fertig ist.
  
'''Important:''' The main thread should never wait for another thread. Instead use Synchronize (see above).
+
'''Wichtig:''' Der Main-Thread sollte niemals auf einen anderen Thread warten müssen. Für diesen Fall benutzen Sie am Besten Synchronize (oben beschrieben).
  
See for an example: lazarus/examples/multithreading/waitforexample1.lpi
+
Beispiel: lazarus/examples/multithreading/waitforexample1.lpi
  
<pre>
+
<syntaxhighlight lang=pascal>
 
{ TThreadA }
 
{ TThreadA }
  
 
procedure TThreadA.Execute;
 
procedure TThreadA.Execute;
 
begin
 
begin
   Form1.ThreadB:=TThreadB.Create(false);
+
   Form1.ThreadB := TThreadB.Create(false);
 
   // create event
 
   // create event
   WaitForB:=RTLEventCreate;
+
   WaitForB := RTLEventCreate;
 
   while not Application.Terminated do begin
 
   while not Application.Terminated do begin
 
     // wait infinitely (until B wakes A)
 
     // wait infinitely (until B wakes A)
 
     RtlEventWaitFor(WaitForB);
 
     RtlEventWaitFor(WaitForB);
     writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
+
     WriteLn('A: ThreadB.Counter=', Form1.ThreadB.Counter);
 
   end;
 
   end;
 
end;
 
end;
Line 280: Line 428:
 
   i: Integer;
 
   i: Integer;
 
begin
 
begin
   Counter:=0;
+
   Counter := 0;
 
   while not Application.Terminated do begin
 
   while not Application.Terminated do begin
 
     // B: Working ...
 
     // B: Working ...
Line 289: Line 437:
 
   end;
 
   end;
 
end;
 
end;
</pre>
+
</syntaxhighlight>
 +
 
 +
== Fork ==
 +
 
 +
When forking in a multithreaded application, be aware that any threads created and running BEFORE the fork (or fpFork) call, will NOT be running in the child process. As stated on the fork() man page, any threads that were running before the fork call, their state will be undefined.
 +
 
 +
So be aware of any threads initializing before the call (including on the initialization section). They will NOT work.
 +
 
 +
== Distributed computing ==
 +
 
 +
The next higher steps after multi threading is running the treads on multiple machines.
 +
* You can use one of the TCP suites like synapse, lnet or indy for communications. This gives you maximum flexibility and is mostly used for loosely connected Client / Server applications.
 +
* You can use message passing libraries like [[MPICH]], which are used for HPC (High Performance Computing) on clusters.

Latest revision as of 01:29, 21 February 2020

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) polski (pl) português (pt) русский (ru) slovenčina (sk) 中文(中国大陆)‎ (zh_CN)

Zurück zu den Zusätzlichen Informationen.

Überblick

Diese Seite soll zeigen, wie man unter FreePascal und Lazarus Multithread-Anwendungen erstellt und verwaltet. In einer Multithread-Anwendung lassen sich verschiedene Aufgaben auf mehrere Threads verteilen, die gleichzeitig ausgeführt werden können.

Wenn Sie bisher keinerlei Erfahrungen zur Multithread-Programmierung gemacht haben, empfehlen wie Ihnen, sich zunächst den Artikel "Benötigt Ihre Anwendung wirklich Multithread-Eigenschaften?" sorgfältig durch zu lesen, da Multithread-Programmierung kein leichtes Unterfangen ist.

Das Hauptziel der Multithread-Programmierung ist die Verfügbarkeit der Benutzeroberfläche eines Programms, während es im Hintergrund Berechnungen durchführt. Dies kann man erreichen, indem man die Berechnung in einen Thread außerhalb des sogenannten Main-Thread verlagert, welcher für die Aktualisierung der Benutzeroberfläche zuständig ist.

Andere Anwendungen, bei denen Multithread-Programmierung zum Einsatz kommt, sind Server-Anwendungen, die mehrere Klienten gleichzeitig betreuen müssen.

Multithread-Anwendungen ermöglichen auch die Aufteilungen der Lasten einer Berechnung auf mehrere Kerne einer Multi-Core-CPU.

Wichtig: Der Main Thread wird beim Start Ihrer Anwendung vom Betriebssystem erstellt. Der Main Thread ist dabei der einzige Thread (und muss auch der einzige bleiben), der für die Aktualisierung der Komponenten der Benutzeroberfläche zuständig ist (Forms, etc.) (ansonsten hängt sich Ihre Anwendung auf).

Benötigt Ihre Anwendung wirklich Multithread-Eigenschaften?

Wenn Multithread Programmierung für Sie Neuland ist und Sie lediglich eine bessere Reaktionsfähigkeit ihrer Anwendung während langer Berechnungen benötigen, dann ist Multithreading nicht unbedingt die einfachste Lösung. Multithreading Applikationen sind immer schwieriger zu debuggen und auch oft viel komplexer. Außerdem benötigen Sie in vielen Fällen kein Multithreading, um ihre Anwendung reaktionsfähig zu halten. Statt dessen können Sie ein Application.ProcessMessages in Ihre Berechnungen mit einbauen. Dieses verarbeitet alle anstehenden Nachrichten, die an Ihre Applikation gesendet wurden und Ihre Applikation reagiert auf Ereignisse. Sie können also einen Teil der Berechnung durchführen, dann Application.ProcessMessages aufrufen und die Benutzereingaben werden verarbeitet und die Oberfläche neu gezeichnet. Platzieren Sie das Application.Processmessages z.B. in Schleifen, so dass bei jedem Schleifendurchlauf die Benutzeroberfläche reagiert.

Zum Beispiel: In den Lazarus-Beispielen unter examples/multithreading/singlethreadingexample1.lpi wird eine große Datei gelesen und verarbeitet und die oben genannte Technik benutzt.

Multithreading wird wirklich benötigt für:

  • blocking handles, wie Netzwerkkommunikation, Interrupts (Linux)
  • Mehrprozessor- oder Mehrkernbetrieb
  • Algorithmen und Bibliotheksaufrufe, die nicht in mehrere kleine Stufen zerteilt werden können.

Die Klasse TThread

Das folgende Beispiel ist im Verzeichnis examples/multithreading/ zu finden. Der einfachste Weg, um eine Multithread-Anwendung zu erstellen, ist die Verwendung der Klasse TThread. Über diese Klasse lässt sich, neben dem Main-Thread, ein zusätzlicher Thread in einfacher Weise erstellen. Unter normalen Umständen müssen dazu bloß zwei Methoden überschrieben (override) werden: Der Konstruktor Create und die Methode Execute. Im Konstruktor Create wird der Thread für die spätere Ausführung vorbereitet. Hier werden die dem Thread zugrunde liegenden Variablen initialisiert. Der originale Konstruktor des Threads enthält einen Parameter namens "Suspended": Damit der Thread nicht automatisch nach seiner Erstellung gestartet wird, ist es empfehlenswert, diesen Parameter auf "true" zu setzen. In diesem Fall können Sie den Thread zu einem späteren Zeitpunkt über die Methode "Resume" starten. Ist es dagegen erwünscht, dass der Thread direkt nach seiner Erstellung startet, setzen Sie Suspended auf "false".

Ab der Version 2.0.1 des FreePascal-Compilers lässt sich auch die Größe des von einem Thread verwendeten Stacks über einen Parameter des Konstruktor TThread.Create einstellen. Das kann beispielsweise bei rekursiven Prozeduren oder Funktionen nützlich sein. Wird die Größe des Stack nicht explizit festgelegt, verwendet FPC die vom Betriebssystem festgelegte Standardgröße.

In die (mit Parameter Override) überschriebene Methode Execute schreibt man den vom Thread auszuführenden Quelltext hinein.

Die Klasse TThread besitzt die wichtige Eigenschaft (property): Terminated : boolean; (Standardeinstellung: Terminated=false)

Der Sinn der Eigenschaft Terminated ist, dass sich ein Thread damit zu einem beliebigen Zeitpunkt abbrechen lässt. Jedoch muss dies von dem Programmierer in den Quelltext der Methode Execute eingearbeitet werden - die Methode Terminate greift also nicht direkt in den vom Programmierer vorgesehenen Programmablauf ein. Sollten sich in Ihrem Thread also Repeatschleifen oder andere sich wiederholende Stellen finden, ist es deshalb notwendig, dass Sie in jeder Schleife explizit prüfen, ob die Eigenschaft Terminated wahr oder falsch ist. Sollte Terminated=true sein, muss die Methode Execute (evtl. nach einer finalen Konfiguration) auf schnellstem Wege verlassen werden.

Wie eingangs erwähnt, sollte der erstellte Thread in keinem Fall mit den sichtbaren Komponenten der Benutzeroberfläche agieren. Die Benutzeroberfläche verändern, Eingaben abfragen, etc. darf ausschließlich der Main-Thread.

Um trotzdem aus einem Thread heraus auf die Benutzeroberfläche bzw. Variablen des Hauptprogrammes zuzugreifen, existiert die Methode Synchronize. Synchronize wird mit einer procedure als Parameter aufgerufen. Wenn Sie Synchronize(@MyMethod) aufrufen, wird der Thread gestoppt, der Code in MyMethod wird im Hauptthread ausgeführt, und dann die Bearbeitung des Threads fortgesetzt. Die exakte Arbeitsweise von Synchronize hängt von der Plattform ab.

Eine andere wichtige Eigenschaft (property) von TThread ist FreeOnTerminate. Wenn diese Eigenschaft auf True gesetzt ist, wird der Thread automatisch freigegeben (wie ein Aufruf von .Free), wenn Execute beendet wird. Beispiel:

  Type
    TMyThread = class(TThread)
    private
      fStatusText : string;
      procedure ShowStatus;
    protected
      procedure Execute; override;
    public
      Constructor Create(CreateSuspended : boolean);
    end;

  constructor TMyThread.Create(CreateSuspended : boolean);
  begin
    FreeOnTerminate := True;
    inherited Create(CreateSuspended);
  end;

  procedure TMyThread.ShowStatus;
  // Diese Methode wird vom MainThread ausgeführt und kann deshalb auf alle GUI-Elemente zugreifen
  begin
    Form1.Caption := fStatusText;
  end;
 
  procedure TMyThread.Execute;
  var
    newStatus : string;
  begin
    fStatusText := 'TMyThread Starting...';
    Synchronize(@Showstatus);
    fStatusText := 'TMyThread Running...';
    while (not Terminated) and ([any condition required]) do
      begin
        ...
        // here goes the code of the main thread loop
        ...
        if NewStatus <> fStatusText then
          begin
            fStatusText := newStatus;
            Synchronize(@Showstatus);
          end;
      end;
  end;

In der Hauptunit erfolgt dann folgender Aufruf

  var
    MyThread : TMyThread;
  begin
    MyThread := TMyThread.Create(True); // So startet es nicht automatisch
    ...
    // Here the code initialises anything required before the threads starts executing
    ...
    MyThread.Resume;
  end;

Soll Ihr Programm flexibler sein, können Sie ein Ereignis für den Thread erzeugen - Ihre synchronisierte Methode ist dann nicht eng an eine bestimmte Form oder Klasse gebunden und Listeners können mit dem Ereignis des Threads verbunden werden. Hier ein Beispiel:

  Type
    TShowStatusEvent = procedure(Status: String) of Object;

    TMyThread = class(TThread)
    private
      fStatusText : string;
      FOnShowStatus: TShowStatusEvent;
      procedure ShowStatus;
    protected
      procedure Execute; override;
    public
      Constructor Create(CreateSuspended : boolean);
      property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
    end;

  constructor TMyThread.Create(CreateSuspended : boolean);
  begin
    FreeOnTerminate := True;
    inherited Create(CreateSuspended);
  end;

  procedure TMyThread.ShowStatus;
  // Diese Methode wird vom MainThread ausgeführt und kann deshalb auf alle GUI-Elemente zugreifen
  begin
    if Assigned(FOnShowStatus) then
    begin
      FOnShowStatus(fStatusText);
    end;
  end;

  procedure TMyThread.Execute;
  var
    newStatus : string;
  begin
    fStatusText := 'TMyThread Starting...';
    Synchronize(@Showstatus);
    fStatusText := 'TMyThread Running...';
    while (not Terminated) and ([any condition required]) do
      begin
        ...
        [here goes the code of the main thread loop]
        ...
        if NewStatus <> fStatusText then
          begin
            fStatusText := newStatus;
            Synchronize(@Showstatus);
          end;
      end;
  end;

Und im Hauptformular

  Type
    TForm1 = class(TForm)
      Button1: TButton;
      Label1: TLabel;
      procedure FormCreate(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
    private
      { private declarations }
      MyThread: TMyThread; 
      procedure ShowStatus(Status: string);
    public
      { public declarations }
    end;

  procedure TForm1.FormCreate(Sender: TObject);
  begin
    inherited;
    MyThread := TMyThread.Create(true);
    MyThread.OnShowStatus := @ShowStatus;
  end;

  procedure TForm1.FormDestroy(Sender: TObject);
  begin
    MyThread.Terminate;
    MyThread.Free;
    inherited;
  end;

  procedure TForm1.Button1Click(Sender: TObject);
  begin
   MyThread.Resume;
  end;

  procedure TForm1.ShowStatus(Status: string);
  begin
    Label1.Caption := Status;
  end;

Worauf Sie achten sollten

Wenn Sie den Compilerswitch -Ct (Stack Check) benutzen, kann das einige Probleme mit sich bringen (Originalübersetzung: Es könnte Ihnen Kopfschmerzen bereiten :)) Aus unbekannten Gründen "triggert" der Stack Check an jedem TThread.Create, wenn Sie die Standardgröße des Stacks benutzen. Die einzige Lösung ist zur Zeit, -Ct nicht zu benutzen.

Um auf Exceptions im Thread zu prüfen, können Sie folgendermaßen vorgehen:

     MyThread := TThread.Create(False);
     if Assigned(MyThread.FatalException) then
       raise MyThread.FatalException;

Dieser Code sorgt dafür daß Exceptions, die während der Erzeugung des Threads auftreten, im Main Thread einen Ausnahmefehler auslösen.

Für Multithread-Anwendungen benötigte Units

Unter Windows benötigen Sie keine spezielle Unit, damit es funktioniert. Unter Linux, MacOSX und FreeBSD benötigen Sie die Unit cthreads und diese muss die erste verwendete Unit im Projekt sein (die Programm-Unit, .lpr)!

Daher sollte der Code Ihrer Lazarus-Anwendung etwa so aussehen:

  program MyMultiThreadedProgram;
  {$mode objfpc}{$H+}
  uses
  {$ifdef unix}
    cthreads,
  {$endif}
    Interfaces, // dies bindet das LCL Widgetset ein
    Forms
    { fügen Sie Ihre Units hier hinzu },

Wenn Sie dies vergessen, kommt die folgende Fehlermeldung:

 This binary has no thread support compiled in.
 Recompile the application with a thread-driver in the program uses clause before other units using thread.

Lazarus Packages, die Multithreading beutzen, sollten -dUseCThreads zu den benutzerdefinierten Einstellungen für abhängige Pakete hinzufügen. Dazu öffnen Sie den Package-Editor für das entsprechende Paket, unter Einstellungen > Bedienung > Benutzerdefiniert fügen Sie -dUseCThreads hinzu. Dies wird diesen Compiler-Switch für alle Projekte und andere Packages, die dieses Package benutzen (die IDE eingeschlossen), definieren. Sowohl die IDE als auch alle von ihr neu erstellten Projekte, haben bereits folgenden Code in ihrer Projektdatei (.lpr):

  uses
    {$IFDEF UNIX}{$IFDEF UseCThreads}
    cthreads,
    {$ENDIF}{$ENDIF}

SMP (Mehrprozessor) Unterstützung

Sobald Sie Threads benutzen, werden diese vom Betriebsystem auf mehrere Prozessoren verteilt.

Debuging Multithread-Anwendungen mit Lazarus

Das Debuggen von Multithread-Anwendungen wird von Lazarus zur Zeit noch nicht vollständig unterstützt.

Debugger Ausgabe

In einer Anwendung mit einem Thread (dem Hauptthread), können Sie einfach auf die Konsole in ein Terminal oder ähnliches schreiben und die Reihenfolge der Zeilen wird chronologisch so angeordnet sein, wie Sie es ausgegeben haben. In Multithreaded Anwendungen wird dies etwas komplizierter. Wenn 2 Threads schreiben, sagen wir, eine Zeile wird von Thread A und eine von Thread B geschrieben, dann ist nicht gesagt, dass sie zeitlich in der richtigen Reihenfolge erscheinen, bzw. es kann vorkommen, dass ein Thread dem anderen in die Ausgabe hineinschreibt.

Die Unit LCLProc enthält einige Funktionen, um jeden Thread seine eigene Logdatei schreiben zu lassen:

  procedure DbgOutThreadLog(const Msg: string); overload;
  procedure DebuglnThreadLog(const Msg: string); overload;
  procedure DebuglnThreadLog(Args: array of const); overload;
  procedure DebuglnThreadLog; overload;

Zum Beispiel: Anstelle von writeln('irgendein Text ', 123); verwenden Sie

  DebuglnThreadLog(['irgendein Text ', 123]);

Dies wird eine Zeile 'irgendein Text 123' an die Log<PID>.txt Datei anhängen, wobei <PID> die Prozess ID des aktuellen Threads ist.

Es ist eine gute Idee, die Logdateien vor jedem Lauf zu entfernen:

  rm -f Log* && ./project1

Linux

Das Debuggen von grafischen Multithreading-Anwendungen unter Linux funktioniert mittlerweile problemlos. In der Vergangenheit traten aber aufgrund von Kompatibilitätsproblemen dabei Probleme mit dem X-Server auf: Er stürzte ab.

Sollten bei Ihnen also solche Probleme auftreten, können Sie diese durch Starten einer neuen X-Instanz umgehen. Wie Sie dies erreichen, ist im Folgenden dargestellt. Eine andere Lösung ist bisher nicht bekannt.

X-Server in einem neuen Terminal starten

Starten Sie eine neue X-Instanz. Dies können Sie über die Konsole mit folgendem Befehl erreichen:

 X :1 &

Nachdem Sie diesen Befehl ausgeführt haben, öffnet sich die neue X-Instanz. Mit den Tastenkombinationen [Strg]+[Alt]+[F7] und [Strg]+[Alt]+[F8] können Sie nun zwischen der Arbeitsoberfläche, auf der Sie arbeiten, und der neuen Instanz hin und her wechseln. (Bei der Slackware-Distribution und wenigen anderen wird dies durch die Kombination [Strg]+[Alt]+[F2] erreicht)

Als nächsten Schritt müssen Sie eine Desktop-Sitzung in der neuen X-Instanz starten. Starten Sie zum Beispiel eine neue gnome-Sitzung in der Konsole:

 gnome-session --display=:1 &

X-Server in einem Fenster starten

Mit Xnest oder Xephyr können Sie auch einen X-Server in einem Fenster eines anderen X-Servers betreiben. Der Vorteil ist, dass Sie beim Debuggen nicht mehr zwischen verschiedenen Terminals wechseln müssen.

Mit dem Befehl

 Xnest :1 -ac

starten Sie mit Xnest eine X-Sitzung auf :1 und deaktivieren die Zugriffskontrolle.

Unter GNOME können Sie auch folgenden Befehl verwenden:

 gdmflexiserver --xnest

Dies startet nicht nur eine X-Sitzung; es wird auch gleichzeitig eine neue GDM-Sitzung, in der Sie sich anmelden können, gestartet.

Benötigte Einstellungen in Lazarus

Schließlich müssen Sie eine letzte Einstellung in Lazarus selbst vornehmen: Gehen Sie in der IDE in das Menü "Start"->"Start-Parameter...". Aktivieren Sie dort das Häkchen "Display verwenden", und tragen Sie in das Feld darunter die Zuordnungsnummer ":1" ein.


Jetzt wird die Anwendung auf dem zweiten X Server laufen und Sie sind in der Lage, sie vom ersten aus zu debuggen.

Dies wurde mit Free Pascal 2.0 und Lazarus 0.9.10 unter Windows und Linux getestet.

Widgetsets

Die win32, gtk und carbon Schnittstellen unterstützen Multithreading vollständig. Das bedeutet, TThread, critical sections und Synchronize funktioniert.

Critical sections

Eine critical section (kritischer Abschnitt) stellt sicher, dass in einer multi-thread Anwendung nicht gleichzeitig von verschiedenen Threads aus auf einen bestimmten Bereich des Codes zugegriffen wird. Bevor eine critical section benutzt werden kann, muss sie erstellt und initialisiert werden; wenn sie nicht mehr gebraucht wird, muss sie freigegeben werden.

Zur Verwendung von critical sections gehen Sie wie folgt vor:

Fügen Sie über uses die Unit SyncObjs hinzu, die die notwendigen Routinen beinhaltet.

Als nächsten Schritt deklarieren Sie die critical section (für alle Threads, die auf die section Zugriff haben sollen):

  MyCriticalSection: TRTLCriticalSection;

Erstellen Sie die Section mit folgender Prozedur:

 InitializeCriticalSection(MyCriticalSection);


Starten Sie einige Threads, die exklusive Aufgaben durchführen:

  EnterCriticalSection(MyCriticalSection);
  try
    // An dieser Stelle können Sie auf Variablen zugreifen, Dateien schreiben, auf das Netzwerk zugreifen, u.s.w.
  finally
    LeaveCriticalSection(MyCriticalSection);
  end;

Nachdem alle Threads, die Zugriff auf die critical section hatten, beendet wurden, müssen Sie sie nun deinitialisieren:

  DeleteCriticalSection(MyCriticalSection);

Alternativ können Sie die Klasse TCriticalSection benutzen. Das Erstellen eines Objektes der Klasse entspricht hier der Initialisierung, die Methode Enter entspricht obigem EnterCriticalSection, die Methode Leave entspricht der obigen LeaveCriticalSection und die Vernichtung (destruction) des Objektes erledigt das Freigeben.

Beispiel: 5 Threads, die einen einzigen Zähler erhöhen: lazarus/examples/multithreading/criticalsectionexample1.lpi

ACHTUNG: Sowohl die RTL als auch die LCL beinhalten beide obige 4 Funktionen. (Die Funktionen der LCL sind in den Units LCLIntf und LCLType definiert.) Beide erfüllen den gleichen Zweck. Sie können sowohl die Funktionen der RTL als auch die Funktionen der LCL zur selben Zeit in Ihrem Programm einsetzen. Sie sollten es nur vermeiden, die "critical section"-Funktionen der LCL auf die Funktionen der RTL anzuwenden und umgekehrt.


Gemeinsamer Variablenzugriff

Mehrere Threads können sich eine Variable im Lesezugriff teilen, ohne dass Probleme zu erwarten sind. Nur wenn der eine oder andere Thread den Wert der Variablen ändern möchte, muss sicher gestellt werden, dass niemals mehrere Threads gleichzeitig auf die Variable zugreifen.


Als Beispiel: 5 Threads inkrementieren einen Counter: lazarus/examples/multithreading/criticalsectionexample1.lpi

Wenn ein Thread das Ergebnis eines anderen benötigt...

Wenn ein Thread A das Ergebnis eines Threads B benötigt, muss er warten, bis B fertig ist.

Wichtig: Der Main-Thread sollte niemals auf einen anderen Thread warten müssen. Für diesen Fall benutzen Sie am Besten Synchronize (oben beschrieben).

Beispiel: lazarus/examples/multithreading/waitforexample1.lpi

{ TThreadA }

procedure TThreadA.Execute;
begin
  Form1.ThreadB := TThreadB.Create(false);
  // create event
  WaitForB := RTLEventCreate;
  while not Application.Terminated do begin
    // wait infinitely (until B wakes A)
    RtlEventWaitFor(WaitForB);
    WriteLn('A: ThreadB.Counter=', Form1.ThreadB.Counter);
  end;
end;

{ TThreadB }

procedure TThreadB.Execute;
var
  i: Integer;
begin
  Counter := 0;
  while not Application.Terminated do begin
    // B: Working ...
    Sleep(1500);
    inc(Counter);
    // wake A
    RtlEventSetEvent(Form1.ThreadA.WaitForB);
  end;
end;

Fork

When forking in a multithreaded application, be aware that any threads created and running BEFORE the fork (or fpFork) call, will NOT be running in the child process. As stated on the fork() man page, any threads that were running before the fork call, their state will be undefined.

So be aware of any threads initializing before the call (including on the initialization section). They will NOT work.

Distributed computing

The next higher steps after multi threading is running the treads on multiple machines.

  • You can use one of the TCP suites like synapse, lnet or indy for communications. This gives you maximum flexibility and is mostly used for loosely connected Client / Server applications.
  • You can use message passing libraries like MPICH, which are used for HPC (High Performance Computing) on clusters.