Threads et TEvents

Adrien Reboisson

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.

- Titre : Threads et TEvents

- Auteur : Adrien Reboisson

- Parution : 12 septembre 2004

- Mise à jour : 7 décembre 2019

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

Sommaire

- I. But recherché

- II. Utilisation du TEvent

- III. Implémentation

- III-A. Définition du thread

- III-B. Constructeur du thread

- III-C. Destructeur du thread

- III-D. La procédure Execute() du thread

- III-E. 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 minute, 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); //"Dormir" 30 minutes
DoWork(); //Cette méthode contient le code à exécuter
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 appelé et que le temps d'attente est 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 envisageables, 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 appelant jusqu'à ce que l'objet passe à l'état activé (ce qui implique qu'un autre thread l'active pendant que le premier thread attend). 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 a 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éé 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

III-A - 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éé. Par convention, on fait précéder d'un « F » les propriétés d'un objet : j'appellerai donc cet événement FEvent.

III-B - 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, ''); { Créer l'événement désactivé }
inherited Create(ACreateSuspended); //appeler la méthode ancêtre
end;

III-C - 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;

III-D - 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; { Intervalle d'exécution. Ici, 120 secondes }
while not Terminated do
try
case FEvent.WaitFor(LSecondsToWait * 1000) of
wrSignaled : Terminate; { Evènement déclenché. Demande de fin de thread. On sort de la boucle }
wrTimeout : DoWork; { Délai atteint : on exécute la procédure }
end;
finally
end;
end;

Autrement dit : tant que personne n'a demandé la terminaison du thread, on exécute la boucle while. On attend 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 met la propriété interne du thread Terminated à True afin de sortir de la boucle while. Le thread est par conséquent terminé.

III-E - La procédure Kill() du thread

Cette procédure (que j'ai nommée ici Kill(), mais que vous pouvez appeler comme vous souhaitez) permet de demander l'arrêt du thread en activant l'événement. Rien de sorcier, il suffit d'utiliser la méthode SetEvent du TEvent :

delphi

procedure TEventThread.Kill;
begin
FEvent.SetEvent; { L'événement est ainsi activé }
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'appeler 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

{$R *.dfm}

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;  { Evènement interne }
FInterval : Cardinal; { Intervalle }
FOnTimer: TNotifyEvent;  { Procédure d'événement à appeler pour exécuter le code }
protected
procedure Execute; override;
procedure DoTimer; { Wrapper pour FOnTimer }
public
constructor Create(ACreateSuspended : Boolean; AInterval : Cardinal);  { Constructeur }
destructor Destroy; override; { Destructeur }
procedure Terminate; reintroduce; { Procédure de terminaison à nous, redéfinie }
property Terminated; { réexpose la propriété ancètre }
property OnTimer : TNotifyEvent read FOnTimer write FOnTimer;  { Propriété
permettant la gestion de l'événement utilisateur FOnTimer }
end;

implementation

constructor TThreadedTimer.Create(ACreateSuspended : Boolean; AInterval : Cardinal);
begin
FEvent := TEvent.Create(nil, False, False, ''); { Créer l'événement désactivé }
FInterval := AInterval; { Mémoriser l'intervalle }
inherited Create(ACreateSuspended); { Appeller la méthode ancêtre }
end;

destructor TThreadedTimer.Destroy;
begin
FreeAndNil(FEvent); { Détruire l'événement }
inherited;
end;

procedure TThreadedTimer.DoTimer;
begin
if Assigned(FOnTimer) then { procédure assignée ?... }
FOnTimer(Self); { ... alors l'exécuter }
end;

procedure TThreadedTimer.Execute;
begin
while not Terminated do
try
case FEvent.WaitFor(FInterval) of
wrSignaled : Terminate; { Evènement déclenché. Demande de fin de thread. On sort de la boucle }
wrTimeout : DoTimer; { Délai atteint : on exécute la procédure }
end;
finally
end;
end;

procedure TThreadedTimer.Terminate;
begin
FEvent.SetEvent; { Activer l'evénement }
end;

Un petit exemple, permettant l'affichage d'une boîte de dialogue toutes les 10 secondes (notez l'usage de l'API MessageBox au lieu des fonctions de la VCL, car la méthode sera appelée depuis un thread) :

delphi

var
Form1: TForm1;
GEventThread : TThreadedTimer = nil;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
{ procédure appelée pour arrêter le thread }
GEventThread.Terminate;
MessageDlg('Thread stoppé.', mtWarning, [mbOK], 0);
end;

procedure TForm1.DoWork(sender : tobject);
begin
{ code à exécuter dans notre procédure de thread. Attention, ce code est threadé. }
MessageBox(Handle, pchar('HelloWorld'), nil, 0);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
{ Associer l'événement }
GEventThread.OnTimer := DoWork;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
{ Terminer le thread si besoin }
if not GEventThread.Terminated then
GEventThread.Terminate;
end;

initialization
GEventThread := TThreadedTimer.Create(False, 10 * 1000); {Toutes les 10 secondes }
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.