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
Sélectionnez
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 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

III.1. 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.

III.2. 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
Sélectionnez
constructor TEventThread.Create(ACreateSuspended : Boolean);
begin
  FEvent := TEvent.Create(nil, False, False, ''); { Créer l'événement désactivé }
  inherited Create(ACreateSuspended); //appeller la méthode ancètre
end;			

III.3. Destructeur du thread

La seule tâche à faire ici est de détruite notre objet FEvent :

delphi
Sélectionnez
destructor TEventThread.Destroy;
begin
  FreeAndNil(FEvent);
  inherited;
end;			

III.4. 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
Sélectionnez
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 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é.

III.5. 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
Sélectionnez
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'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
Sélectionnez
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
Sélectionnez
type
  TThreadedTimer = class(TThread)
  private
    FEvent : TEvent;  { Evènement interne }
    FInterval : Cardinal; { Intervalle }
    FOnTimer: TNotifyEvent;  { Procédure d'événement à appeller 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 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
Sélectionnez
var
  Form1: TForm1;
  GEventThread : TThreadedTimer = nil;
 
implementation
 
{$R *.dfm}
 
procedure TForm1.Button1Click(Sender: TObject);
begin
  { procédure appellé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.