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