Difference between revisions of "Multithreaded Application Tutorial/fr"

From Lazarus wiki
Jump to navigationJump to search
Line 63: Line 63:
 
Pour faire ceci , une méthode de la classe TThread appelée Synchronize existe .
 
Pour faire ceci , une méthode de la classe TThread appelée Synchronize existe .
 
Synchronize exige une méthode (qui ne prend aucun paramètre ) comme argument .  
 
Synchronize exige une méthode (qui ne prend aucun paramètre ) comme argument .  
Quand vous appelez cette méthode au moyen de Synchronize(@MyMethod), l'execution de la tâche sera mis en pause, le code de  MyMethod s'executera dans la tâche principale, et ensuite l'execution de la tâche sera repris. Le fonctionnement exact de  Synchronize dépend de la plate-forme, mais fondamentalement il fait ceci: Il signale un message sur la file d'attente de message principale et va dormir. 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.
+
Quand vous appelez cette méthode au moyen de Synchronize(@MyMethod), l'execution de la tâche sera mis en pause, le code de  MyMethod s'executera dans la tâche principale, et ensuite l'execution de la tâche sera repris. Le fonctionnement exact de  Synchronize dépend de la plate-forme, mais fondamentalement il fait ceci: Il signale un message sur la file d'attente de message principale et va dormir. Par la suite la tâche principale traite le message et appelle MyMethod. De cette façon  MyMethod est appelé hors contexte, ce qui signifie pas pendant un évènement de déplacement de souris vers le bas ou pendant un événement de peinture, mais après . Après que la tâche principale a executé MyMethod, il réveille la tâche en sommeil et exécute le message suivant. La tâche continue alors .
  
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.
+
Il y a une autre propriété importante de la classe TThread: FreeOnTerminate. Si cette propriété est true, l'objet tâche est automatiquement libéré quand l'execution de la tâche (méthode .Execute) s'arrête. Autrement  l'application devra le libérer manuellement.
  
Example:
+
Exemple :
  
 
   Type
 
   Type
Line 112: Line 112:
 
   end;
 
   end;
  
On the application,
+
Sur l'application ,
  
 
   var
 
   var
Line 124: Line 124:
 
   end;
 
   end;
  
If you want to make your application more flexible you can create an event for the thread - this way your synchronized method won't be tightly coupled with a specific form or class - you can attach listeners to the thread's event. Here is an example:
+
Si vous voulez rendre votre demande plus flexible vous pouvez créer un événement pour la tâche  - de cette façon votre méthode synchronized ne sera pas étroitement couplée  avec une fiche spécifique ou une classe - you can attach listeners to the thread's event. Voici un exemple :
  
  
Line 177: Line 177:
 
   end;
 
   end;
  
On the application,
+
Sur l'application ,
  
 
   Type
 
   Type

Revision as of 11:32, 27 June 2007

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

Vue d'ensemble

Cette page essayera d'expliquer comment écrire et corriger une application multi-tâches avec Free Pascal et Lazarus.

Une application multi-tâches est celle qui crée deux tâches d'execution ou plus en même temps.

Si vous êtes un nouveau dans le multi-tâches,veuillez lire le paragraphe "Avez vous besoin du multi-tâches ?" pour découvrir , si vous avez vraiment besoin de ça. Vous pouvez vous éviter beaucoup de maux de tête .

Une des tâches légères s'appelle la tâche principale . La tâche principale est celle qui est créé par le logiciel d'exploitation , une fois que notre application commence . La tâche principale doit être la seule tâche qui met à jour les composants qui interfèrent avec l'utilisateur (autrement , l'application peut accrocher ).

L'idée principale est que l'application peut faire quelques traitements en arrière-plan (dans une deuxième tâche) tandis que l'utilisateur peut continuer à travailler (en employant la tâche principale).

Une autre utilisation des tâches est juste pour avoir une application qui répond mieux. Si vous créez une application, et quand l'utilisateur appuie sur un bouton, l'application commence le traitement(un gros travail )... et pendant le traitement , L'écran arrête de répondre, et donne à l'utilisateur la sensation que l'application est morte,ce n'est pas si agréable. Si le gros travail fonctionne dans une deuxième tâche, l'application continue à répondre (presque ) comme si elle était en idle. Dans ce cas-ci c'est une bonne idée, avant de commencer la tâche, pour neutraliser les boutons de la fiche pour éviter à l'utilisateur de commencer plus d'une tâche pour le travail .

Une autre utilisation , est de créer une application serveur qui est capable de répondre à beaucoup de clients en même temps .

Avez vous besoin du multi-tâche?

Si vous êtes un novice avec le multi-tâche et vous voulez seulement rendre votre application plus nerveuse tandis que votre application calcule une grande charge de travail, alors le multi-tâche ne devrait pas être, ce que vous recherchez. Les applications multi-tâches sont toujours plus difficile à debogguer et elles sont souvent beaucoup plus complexes. Et dans beaucoup de cas vous n'avez pas besoin du multi-tâche . Une seule tâche est suffisante. Si vous pouvez fractionner les longues corvées de travail en plusieurs petits morceaux, alors à la place vous devriez employer Application.ProcessMessages. Cette méthode laisse la LCL traiter tous les messages d'attente et ceux de retour . L'idée est de traiter une partie du travail, puis d'appeller Application.ProcessMessages pour voir si l'utilisateur abandonne ou clique quelque part ou l'indicateur de processus est actualisé, ensuite continuer avec la partie suivante du travail, appeller Application.ProcessMessages et ainsi de suite.

Par exemple : Lecture d'un grand fichier et son traitement . Voir examples/multithreading/singlethreadingexample1.lpi.

Le multi-tâche est seulement nécessaire pour

  • le blocage des handles, comme les communications de réseau
  • l'utilisation de plusieurs processeurs à la fois
  • les algorithmes et les appels de bibliothèque, qui ne peuvent pas être fractionné dans de petites pièces.

La classe TThread

L'exemple suivant peut être trouvé dans le répertoire examples/multithreading/.

Pour creer une application multi-tâches, la manière la plus facile est d'employer la classe TThread.

Cette classe permet la création d'une tâche supplémentaire(à côté de la tâche principale) de manière simple .

Normalement, vous devez seulement surcharger 2 méthodes: le constructeur Create, et la méthode Execute.

Dans le constructeur, vous préparerez la tâche à s'executer. Vous fixerez les valeurs initiales des variables ou propriétés que vous avez besoin. Le constructeur original de TThread exige un paramètre appelé Suspended. Comme vous pourriez le prévoir, fixer Suspended = True empêchera la tâche de commençer automatiquement après sa création . Si Suspended = False, la tâche commencera à fonctionner juste aprés sa création . Si la tâche est créé en étant suspendue, alors elle fonctionnera seulement après que la méthode Resume soit appelée.

A partir de FPC version 2.0.1 et au delà, TThread.Create a également un paramètre implicite pour la taille de la pile . Vous pouvez maintenant changer la taille de la pile par défaut de chaque tâche que vous créez si vous en avez besoin. Les appels de procedure récursifs en profondeur dans une tâche sont un bon exemple. Si vous n'indiquez pas le paramètre de la taille de la pile, une taille de pile de l'OS par défaut est employée.

Dans la méthode surchargée Execute vous écrirez le code qui fonctionnera dans la tâche .

La classe TThread a une propriété importante : Terminated : boolean;

Si la tâche a une boucle (et que c'est habituel), la boucle devrait être quittée quand Terminated vaut true (sa valeur est faux par défaut). Ainsi dans chaque cycle, il doit vérifier si Terminated vaut true, et si c'est le cas, doit sortir de la méthode .Execute aussi rapidement que possible, après tout nettoyage nécessaire .

Aussi gardez à l'esprit que la méthode Terminate ne fait rien par défaut: la méthode .Execute doit explicitement mettre en application un support afin de stopper son travail .

Comme nous avons expliqué plus tôt, la tâche ne devrait pas interagir avec les composants visibles. Pour montrer quelque chose à l'utilisateur il doit le faire dans la tâche principale. Pour faire ceci , une méthode de la classe TThread appelée Synchronize existe . Synchronize exige une méthode (qui ne prend aucun paramètre ) comme argument . Quand vous appelez cette méthode au moyen de Synchronize(@MyMethod), l'execution de la tâche sera mis en pause, le code de MyMethod s'executera dans la tâche principale, et ensuite l'execution de la tâche sera repris. Le fonctionnement exact de Synchronize dépend de la plate-forme, mais fondamentalement il fait ceci: Il signale un message sur la file d'attente de message principale et va dormir. Par la suite la tâche principale traite le message et appelle MyMethod. De cette façon MyMethod est appelé hors contexte, ce qui signifie pas pendant un évènement de déplacement de souris vers le bas ou pendant un événement de peinture, mais après . Après que la tâche principale a executé MyMethod, il réveille la tâche en sommeil et exécute le message suivant. La tâche continue alors .

Il y a une autre propriété importante de la classe TThread: FreeOnTerminate. Si cette propriété est true, l'objet tâche est automatiquement libéré quand l'execution de la tâche (méthode .Execute) s'arrête. Autrement l'application devra le libérer manuellement.

Exemple :

 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;
 // this method is executed by the mainthread and can therefore access all GUI elements.
 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;

Sur l'application ,

 var
   MyThread : TMyThread;
 begin
   MyThread := TMyThread.Create(True); // This way it doesn't start automatically
   ...
   [Here the code initialises anything required before the threads starts executing]
   ...
   MyThread.Resume;
 end;

Si vous voulez rendre votre demande plus flexible vous pouvez créer un événement pour la tâche - de cette façon votre méthode synchronized ne sera pas étroitement couplée avec une fiche spécifique ou une classe - you can attach listeners to the thread's event. Voici un exemple :


 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;
 // this method is executed by the mainthread and can therefore access all GUI elements.
 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;

Sur l'application ,

 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;

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.

Units needed for a multithreaded application

You don´t need any special unit for this to work with Windows. However with Linux, MacOSX and FreeBSD, you need the cthreads unit and it must be the first used unit of the project (the program unit, .lpr)!

So, your Lazarus application code should look like:

 program MyMultiThreadedProgram;
 {$mode objfpc}{$H+}
 uses
 {$ifdef unix}
   cthreads,
 {$endif}
   Interfaces, // this includes the LCL widgetset
   Forms
   { add your units here },

If you forget this you will get this error on startup:

 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 Support

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

Debuging Multithreaded Applications with Lazarus

The debugging on Lazarus is not fully functional yet.

Debugging output

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;

For example: Instead of writeln('Some text ',123); use

 DebuglnThreadLog(['Some text ',123]);

This will append a line 'Some text 123' to Log<PID>.txt, where <PID> is the process ID of the current thread.

It is a good idea to remove the log files before each run:

 rm -f Log* && ./project1

Linux

If you try to debug a multithreaded application on Linux, you will have one big problem: the X server will hang.

It is unknown how to solve this properly, but a workaround is:

Create a new instance of X with:

 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.

Now the application will run on the seccond X server and you will be able to debug it on the first one.

This was tested with Free Pascal 2.0 and Lazarus 0.9.10 on Windows and Linux.

Widgetsets

The win32, the gtk and the carbon interfaces fully support multithreading. 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;