Difference between revisions of "Multithreaded Application Tutorial/de"

From Lazarus wiki
Jump to navigationJump to search
m
Line 34: Line 34:
  
 
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 Constructor Create und die Methode Execute.
 
Unter normalen Umständen müssen dazu bloß zwei Methoden überschrieben (override) werden: Der Constructor Create und die Methode Execute.
 
 
Im Constructor Create wird der Thread für die spätere Ausführung vorbereitet. Hier werden die dem Thread zugrunde liegenden Variablen initialisiert.
 
Im Constructor Create wird der Thread für die spätere Ausführung vorbereitet. Hier werden die dem Thread zugrunde liegenden Variablen initialisiert.
 
Der originale Constructor des Threads enthält 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.
 
Der originale Constructor des Threads enthält 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.
Line 48: Line 44:
 
Wird die Größe des Stack nicht explizit festgelegt, verwendet FPC die vom Betriebssystem festgelegte Standartgröße.
 
Wird die Größe des Stack nicht explizit festgelegt, verwendet FPC die vom Betriebssystem festgelegte Standartgröße.
  
In die (mit Parameter Override) überschriebene Methode Execute schreibt man den vom Thread auszuführenden Quelltext rein.
+
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):
 
Die Klasse TThread besitzt die wichtige Eigenschaft (property):
Line 59: Line 55:
 
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.
 
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.
  
To do this, a TThread method called Synchronize exists.
+
Um trotsdem aus einem Thread heraus auf die Benutzeroberfläche bzw Variablen des Hauptprogrammes zuzugreifen existiert eine methode Synchronize.
Synchronize requires a method (that takes no parameters) as an argument.  
+
Synchronize wird mit einer procedure als Parameter aufgerufen.  
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.
+
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.
 
 
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.
 
  
Example:
+
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
 
   Type
Line 85: Line 80:
  
 
   procedure TMyThread.ShowStatus;
 
   procedure TMyThread.ShowStatus;
   // this method is executed by the mainthread and can therefore access all GUI elements.
+
   // Diese Methode wird vom Hauptthread ausgeführt und kann so ohne Probleme auf GUI Elemente zugreifen.
 
   begin
 
   begin
 
     Form1.Caption := fStatusText;
 
     Form1.Caption := fStatusText;
Line 100: Line 95:
 
       begin
 
       begin
 
         ...
 
         ...
         [here goes the code of the main thread loop]
+
         [Hier kommt der Code hin der im Thread ausgeführt werden soll]
 
         ...
 
         ...
 
         if NewStatus <> fStatusText then
 
         if NewStatus <> fStatusText then
Line 117: Line 112:
 
     MyThread := TMyThread.Create(True); // This way it doesn't start automatically
 
     MyThread := TMyThread.Create(True); // This way it doesn't start automatically
 
     ...
 
     ...
     [Here the code initialises anything required before the threads starts executing]
+
     [Hier kommt der Code hin der alles initialisiert bevor der Thread ausgeführt wird]
 
     ...
 
     ...
 
     MyThread.Resume;
 
     MyThread.Resume;

Revision as of 23:49, 7 January 2007

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

(Seite befindet sich in Übersetzung...)

Ü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 durchzulesen, 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 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. 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.

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

Multithreading wird wirklich benötigt für:

  • blocking handles, like network communications
  • Mehrprozessor- oder Mehrkernbetrieb
  • Algorythmen 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 Constructor Create und die Methode Execute. Im Constructor Create wird der Thread für die spätere Ausführung vorbereitet. Hier werden die dem Thread zugrunde liegenden Variablen initialisiert. Der originale Constructor des Threads enthält 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. 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 Standartgröß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 (ev. 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 trotsdem aus einem Thread heraus auf die Benutzeroberfläche bzw Variablen des Hauptprogrammes zuzugreifen existiert eine 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(Suspended : boolean);
   end;
 constructor TMyThread.Create(CreateSuspended : boolean);
 begin
   FreeOnTerminate := True;
   inherited Create(CreateSuspended);
 end;
 procedure TMyThread.ShowStatus;
 // Diese Methode wird vom Hauptthread ausgeführt und kann so ohne Probleme auf 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
       ...
       [Hier kommt der Code hin der im Thread ausgeführt werden soll]
       ...
       if NewStatus <> fStatusText then
         begin
           fStatusText := newStatus;
           Synchronize(@Showstatus);
         end;
     end;
 end;

On the application,

 var
   MyThread : TMyThread;
 begin
   MyThread := TMyThread.Create(True); // This way it doesn't start automatically
   ...
   [Hier kommt der Code hin der alles initialisiert bevor der Thread ausgeführt wird]
   ...
   MyThread.Resume;
 end;

Special things to take care of

There is a potential headache in Windows with Threads if you use the -Ct (stack check) switch. For reasons not so clear the stack check will "trigger" on any TThread.Create if you use the default stack size. The only work-around for the moment is to simply not use -Ct switch. Note that it does NOT cause an exception in the main thread, but in the newly created one. This "looks" like if the thread was never started.

A good code to check for this and other exceptions which can occur in thread creation is:


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


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

Unter Windows benötigen sie keine spezielle Unit, damit es funktioniert. 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:

 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.

SMP Unterstützung

The good news is that if your application works properly multithreaded this way, it is already SMP enabled!

Debuging Multithread-Anwendungen mit Lazarus

The debugging on Lazarus is not fully functional yet.

Debugger Ausgabe

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. 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:

 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

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:

Erzeugen sie eine neue Instanz von X mit:

 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).

Then you could, if you want, create a desktop session on the X started with:

 gnome-session --display=:1 &

Then, in Lazarus, on the run parameters dialog for the project, check "Use display" and enter :1.

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

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

Widgetsets

Die win32, gtk und carbon Schnittstellen unterstützen multithreading vollständig. This means, TThread, critical sections and Synchronize work.

Critical sections

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. 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:

Add the unit SyncObjs.

Declare the section (globally for all threads which should access the section):

 MyCriticalSection: TRTLCriticalSection;

Create the section:

 InitializeCriticalSection(MyCriticalSection);

Run some threads. Doing something exclusively

 EnterCriticalSection(MyCriticalSection);
 try
   // access some variables, write files, send some network packets, etc
 finally
   LeaveCriticalSection(MyCriticalSection);
 end;

After all threads terminated, free it:

 DeleteCriticalSection(MyCriticalSection);

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.

For example: 5 threads incrementing a counter. 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.


Sharing Variables

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. See lazarus/examples/multithreading/criticalsectionexample1.lpi

Waiting for another thread

If a thread A needs a result of another thread B, it must wait, till B has finished.

Important: The main thread should never wait for another thread. Instead use Synchronize (see above).

See for an example: 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='+IntToStr(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;