Threads et TEvents
Date de publication : 12/09/2004 ,
Date de mise a jour : 12/09/2004
Par
Adrien Reboisson (reisubar.developpez.com)
Cet article présente comment utiliser threads et TEvents pour construire des procédures s'exécutant périodiquement en arrière plan dans vos applications.
I. But recherché
II. Utilisation du TEvent
III. Implémentation
I. Définition du thread
I. Constructeur du thread
III. Destructeur du thread
IV. La procédure Execute() du thread
V. La procédure Kill() du thread
IV. Tests
V. TThreadedTimer, une classe réutilisable
VI. Remarques
I. But recherché
Il est parfois utile d'avoir dans une application qui reste longtemps en mémoire (un service, par exemple) la possibilité de lancer régulièrement (toutes les 1 minutes, 30 minutes, 2 heures...) une même procédure en arrière plan. Celle-ci peut être dédiée à des tâches non prioritaires comme l'optimisation d'une base de données ou l'effacement de fichiers temporaires. La manière la plus basique de procéder est la suivante :
- Créer une classe dérivée de TThread,
- Dans la procédure OnExecute, coder une fonction utilisant Sleep() pour "dormir" un certain temps, puis le code à exécuter, tout cela dans une boucle testant si la valeur Terminated du thread n'est pas à True. Et si tel est le cas, sortir de cette boucle.
Dans ce cas, le code simplifié pourrait correspondre à cela :
delphi procedure TFooThread.Execute;
begin
while not Terminated do
try
Sleep(30 * 60 * 1000);
DoWork();
finally
end;
end;
Cette solution a néanmoins des limites. Lorsque le thread principal de l'application veut mettre fin à votre thread (ce qui est typiquement fait à la fermeture de l'application), celui-ci devra attendre que l'exécution de Sleep() soit terminée. Par conséquent, si sleep() vient juste d'être appellé et que le temps d'attente soit d'une heure, le thread principal de l'application sera en attente du thread secondaire... pendant une heure.
Plutôt gênant. Pour éviter cela, il vaut mieux remplacer le Sleep() par l'appel à une fonction d'attente pouvant être interrompue d'une manière ou d'une autre. Parmi les solutions envisageable, on va choisir l'utilisation d'un TEvent principalement pour la raison que celui-ci est "purement VCL" et peut donc être utilisé au sein d'une application Windows (Delphi) ou Linux (Kylix).
II. Utilisation du TEvent
Un objet TEvent représente un sémaphore binaire utilisable dans le contexte d'une application. Il possède donc deux états : activé et désactivé.
Sa méthode WaitFor permet de bloquer ("faire dormir") le thread appellant jusqu'à que l'objet passe à l'état activé (ce qui implique qu'un autre thread l'active pendant que le premier thread attends). Mais ce qui est intéressant est qu'un timeout peut être défini pour que WaitFor rende la main si l'objet n'a pas été activé pendant un temps d'attente spécifié. Il est ainsi possible de savoir quel événement à provoqué la sortie de WaitFor grâce à son code de retour. On notera les deux plus importants dont nous nous servirons ici : wrSignaled, correspondant à la notification d'activation du signal, et wrTimeOut, signalant que l'objet TEvent n'a pas été activé pendant le temps donné.
Le fonctionnement que nous allons adopter est le suivant : à la création du thread, l'objet événement est crée et désactivé. Dans la boucle principale du thread, nous attendons que l'événement soit déclenché pendant le temps d'attente normalement passé à Sleep(). Nous avons à coder deux éventualités : soit cet événement est lié à la demande de fin de thread, dans ce cas, on le termine; soit cet événement est lié à la fin d'attente sans activation de l'événement, dans ce cas, on continue à attendre.
On créera également une méthode Kill() permettant depuis un thread extérieur d'activer l'événement. Ainsi l'appel de cette méthode déclenchera invariablement la fin immédiate du thread, sauf si le code utilisateur du thread était déjà en cours d'exécution (dans ce cas, il se terminera, puis le thread lui même se terminera).
Dans la partie implémentation, le nom du thread a été arbitrairement choisi à "TEventThread".
III. Implémentation
I. Définition du thread
- Tout d'abord, il faut ajouter SyncObjs dans la clause uses de votre thread. C'est cette unité de la VCL qui gère les objets de synchronisation (et contient donc TEvent).
- Ensuite, il faut déclarer une propriété privée de type TEvent dans le thread crée. Par convention, on fait précéder d'un "F" les propriétés d'un objet : j'appelerais donc cet événement FEvent.
I. Constructeur du thread
Il suffit de créer l'objet dans le constructeur du thread. Les trois paramètres attendus sont respectivement un pointeur sur une structure de type PSecurityAttributes spécifiant les droits d'accès et les possibilités d'héritage de l'objet (initialisé par défaut si le pointeur nil est utilisé), une variable booléenne indiquant si l'événement doit être réinitialisé automatiquement ou pas (pas d'importance dans notre cas car l'événement ne sert qu'une fois), une seconde variable booléenne indiquant si l'événement doit être créé actif ou pas (ici, false, donc), et enfin une variable de type string pouvant être utilisée pour attribuer un nom à l'événement (non utilisé ici) :
delphi constructor TEventThread.Create(ACreateSuspended : Boolean);
begin
FEvent := TEvent.Create(nil, False, False, '');
inherited Create(ACreateSuspended);
end;
III. Destructeur du thread
La seule tâche à faire ici est de détruite notre objet FEvent :
delphi destructor TEventThread.Destroy;
begin
FreeAndNil(FEvent);
inherited;
end;
IV. La procédure Execute() du thread
Cette procédure contenant le code du thread à exécuter par défaut, on place ici notre boucle et l'appel à WaitFor() :
delphi procedure TEventThread.Execute;
var
LSecondsToWait : Integer;
begin
LSecondsToWait := 120;
while not Terminated do
try
case FEvent.WaitFor(LSecondsToWait * 1000) of
wrSignaled : Terminate;
wrTimeout : DoWork;
end;
finally
end;
end;
Autrement dit : tant que personne n'a demandé la terminaison du thread, on exécute la boucle while. On attends que WaitFor de notre FEvent rende la main en lui donnant un délai maximum, ici de 2 minutes. Si le résultat renvoyé est wrTimeOut, c'est que personne n'a demandé la fin du thread : on exécute donc DoWork. Si c'est wrSignaled, alors l'événement a été activé : quelqu'un a demandé la fin du thread. On appelle donc Terminate qui mets la propriété interne du thread Terminated à True afin de sortir de la boucle while. Le thread est par conséquent terminé.
V. La procédure Kill() du thread
Cette procédure (que j'ai nommée ici Kill(), mais que vous pouvez appeller comme vous souhaitez) permet de demander l'arrêt du thread en activant l'événement. Rien de sorcier, il suffit simplement d'utiliser la méthode SetEvent du TEvent :
delphi procedure TEventThread.Kill;
begin
FEvent.SetEvent;
end;
IV. Tests
Pour les tests, il faut dans le thread principal instancier une variable de type TEventThread, puis l'exécuter. Pour que l'on puisse de manière asynchrone demander l'arrêt du thread, créez une variable globale GEventThread instanciée au démarrage de l'application et immédiatement exécutée. Sur l'unique fiche de l'application, posez un bouton et entrez un code qui permettra de tester l'arrêt du thread : il suffit d'appeller Kill pour demander au thread de se terminer, puis la méthode héritée WaitFor de TThread pour attendre la terminaison réelle du thread. On peut comme dans l'exemple afficher un message : cela permet de mesurer de visu le temps de réaction pour la terminaison du thread et ainsi contrôler que notre méthode marche.
delphi var
Form1: TForm1;
GEventThread : TEventThread = nil;
implementation
procedure TForm1.Button1Click(Sender: TObject);
begin
GEventThread.Kill;
GEventThread.WaitFor;
MessageDlg('Thread stoppé.', mtWarning, [mbOK], 0);
end;
initialization
GEventThread := TEventThread.Create(False);
GEventThread.Resume;
finalization
if Assigned(GEventThread) then
FreeAndNil(GEventThread);
end;
V. TThreadedTimer, une classe réutilisable
Pour terminer cet article, on va créer une classe réutilisable permettant l'exploitation des techniques vues. Cette classe permet comme le thread que l'on vient d'aborder d'exécuter périodiquement une même séquence donnée dans un intervalle pouvant être long, tout en n'empêchant pas sa terminaison. Pour faire plus élégant, nous allons introduire quelques légers changements :
- L'intervalle d'exécution sera paramètrable et à préciser dans le constructeur,
- La procédure Terminate sera redéfinie pour permettre automatiquement d'activer l'événement (le rôle de notre précédente méthode Kill),
- La procédure de gestion des événements étant codée dans la procédure Execute du TThreadedTimer, il a fallu déporter l'endroit où placer le code utilisateur autre part. Le code exécuté est donc maintenant géré via l'événement (au sens de pointeur de méthode) OnTimer que les classes dérivées doivent exploiter.
delphi type
TThreadedTimer = class(TThread)
private
FEvent : TEvent;
FInterval : Cardinal;
FOnTimer: TNotifyEvent;
protected
procedure Execute; override;
procedure DoTimer;
public
constructor Create(ACreateSuspended : Boolean; AInterval : Cardinal);
destructor Destroy; override;
procedure Terminate; reintroduce;
property Terminated;
property OnTimer : TNotifyEvent read FOnTimer write FOnTimer;
end;
implementation
constructor TThreadedTimer.Create(ACreateSuspended : Boolean; AInterval : Cardinal);
begin
FEvent := TEvent.Create(nil, False, False, '');
FInterval := AInterval;
inherited Create(ACreateSuspended);
end;
destructor TThreadedTimer.Destroy;
begin
FreeAndNil(FEvent);
inherited;
end;
procedure TThreadedTimer.DoTimer;
begin
if Assigned(FOnTimer) then
FOnTimer(Self);
end;
procedure TThreadedTimer.Execute;
begin
while not Terminated do
try
case FEvent.WaitFor(FInterval) of
wrSignaled : Terminate;
wrTimeout : DoTimer;
end;
finally
end;
end;
procedure TThreadedTimer.Terminate;
begin
FEvent.SetEvent;
end;
Un petit exemple, permettant l'affichage d'une boîte de dialogue toute les 10 secondes (notez l'usage de l'API MessageBox au lieu des fonctions de la VCL, car la méthode sera appellée depuis un thread) :
delphi var
Form1: TForm1;
GEventThread : TThreadedTimer = nil;
implementation
procedure TForm1.Button1Click(Sender: TObject);
begin
GEventThread.Terminate;
MessageDlg('Thread stoppé.', mtWarning, [mbOK], 0);
end;
procedure TForm1.DoWork(sender : tobject);
begin
MessageBox(Handle, pchar('HelloWorld'), nil, 0);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
GEventThread.OnTimer := DoWork;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if not GEventThread.Terminated then
GEventThread.Terminate;
end;
initialization
GEventThread := TThreadedTimer.Create(False, 10 * 1000);
GEventThread.Resume;
finalization
if Assigned(GEventThread) then
FreeAndNil(GEventThread);
VI. Remarques
Le composant JvThreadTimer de la JVCL est développé suivant le même principe, vous proposant des options supplémentaires de paramètrage.
Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur.
La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.
|