Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Delphi 10.3
  3. Traitement de tâches asynchrones
Extrait - Delphi 10.3 Programmation orientée objet en environnement Windows
Extraits du livre
Delphi 10.3 Programmation orientée objet en environnement Windows
9 avis
Revenir à la page d'achat du livre

Traitement de tâches asynchrones

Description d’un processus

Le traitement de tâches asynchrones qu’on appelle aussi multithreading en programmation est le fait de demander au programme d’exécuter plusieurs tâches simultanément. On dit aussi que le programme les exécute en parallèle.

Pour comprendre les mécanismes inhérents au multithreading, il faut comprendre comment se déroule l’exécution d’un processus. On peut facilement associer un exécutable à un processus. Une application simple comporte en général un exécutable et des modules. Elle est décrite au niveau du système d’exploitation par un processus. Une application plus complexe peut être l’association de plusieurs exécutables, mais ceci ne change en rien la compréhension.

Le descripteur le plus utilisé concernant les processus est le ProcessID. Il est utilisé par le Gestionnaire des tâches Windows et permet de déterminer de manière unique un processus. Notez bien que le nom de l’exécutable ne permet pas d’identifier de manière unique un processus car par exemple l’utilisateur peut démarrer plusieurs fois Notepad.exe et seul son ProcessID permet de les distinguer les uns des autres.

Pour obtenir son ProcessID, il faut utiliser l’API GetCurrentProcessID.

images/10EP01.png

Ici plusieurs svchost.exe...

Les threads dans un processus

Le thread est l’entité élémentaire d’exécution de code binaire.

En démarrant, un processus démarre un ou plusieurs threads. L’un d’entre eux est défini comme thread principal et est utilisé pour gérer les entrées/sorties au niveau du système d’exploitation (actions utilisateurs, actions périodiques, messages Windows).

Les descripteurs de thread sont :

  • thread id : utilisé pour différencier les threads les uns des autres.

  • thread handle : utilisé pour manipuler les threads à l’intérieur de l’application. Les API Windows utilisent en général le thread handle comme paramètre d’entrée.

On peut récupérer le thread id du thread principal avec la fonction MainThreadID et les threads id des threads secondaires avec l’API GetCurrentThreadId;.

Le schéma ci-dessous est appelé diagramme de séquence. C’est un diagramme de la norme UML. Ce genre de diagramme représente l’enchaînement des routines par rapport à une action donnéee.

Il représente l’exemple où un processus a instancié deux threads, le Thread ID 1 qui a une durée de vie limitée et le Thread ID2 qui a une durée de vie équivalente à celle du processus....

Pourquoi utiliser du multithreading ?

1. Effectuer plusieurs traitements en parallèle

On utilise du multithreading lorsque les ressources matérielles le permettent. Ce cas est généralement rencontré dans le cadre d’applications avec des contraintes temps réel fortes, comme par exemple une IHM devant présenter des données venant de deux sources différentes évoluant rapidement.

images/10EP03.png

2. Donner une meilleure expérience utilisateur

En Delphi, c’est le thread principal qui est utilisé pour rafraîchir le rendu graphique. Cependant, on peut rafraîchir ce rendu des composants graphiques de la VCL uniquement dans le thread principal.

Imaginons que l’utilisateur souhaite afficher des données issues d’une base de données très volumineuse ou le résultat d’un calcul algorithmique très coûteux. Le temps de réponse de l’application sera similaire au diagramme de séquence suivant qui représente une recherche dans une base de données :

images/10EP04.png

Dans le diagramme ci-dessus sont représentés l’utilisateur (User), la fenêtre de l’application (TMainForm) et la base de données (Database).

L’effet d’attente lors de la recherche dans la base de données donnera lieu à un effet malheureusement assez connu des utilisateurs : un voile blanc...

La classe TThread

Le système d’exploitation Windows fournit à travers des dlls des routines pour créer des threads. La syntaxe est un peu compliquée. Delphi propose une classe TThread qui encapsule les appels au système d’exploitation nécessaires à la manipulation des threads de Windows et simplifie grandement la syntaxe.

Ci-dessous sont listées les méthodes et propriétés les plus couramment utilisées lors de l’implémentation d’un thread :

  • Les principales méthodes utilisées sont :

  • constructor Create(Suspended:boolean); : le paramètre Suspended permet d’indiquer que le thread démarre immédiatement après l’instanciation.

  • procedure Start; : permet de démarrer l’exécution d’un thread dans le cas où il a été créé dans l’état suspended.

  • procedure Suspend; : cette méthode est vue comme dépréciée. Il est recommandé de ne l’utiliser qu’à des fins de débogage. Dans l’absolu, on ne suspend jamais un thread s’il a commencé à s’exécuter.

  • procedure Synchronize; : permet d’exécuter une méthode dans le contexte du thread principal. Cette méthode est très utilisée pour le rafraîchissement...

Thread transitoire TThread

Un thread transitoire est un thread auquel on attache un seul traitement et dont la durée de vie est limitée dans le temps. En général, on utilise ce genre de thread quand les variables nécessaires à l’exécution ne sont pas partagées avec un autre thread ou le thread principal. De même, on utilise également le thread transitoire lorsque le résultat du traitement du thread n’est pas exploité directement dans l’application (comme une compression de fichier par exemple).

Exemple d’implémentation

Reprenons notre exemple avec notre moteur de calcul. On ajoute un autre bouton sur la form pour implémenter un calcul désynchronisé.

procedure TForm1.ComputeAsync; 
  var myComputeThread:TComputeThread; 
begin 
  myComputeThread:=TComputeThread.Create(True); 
  myComputeThread.IterMax := SpinEdit1.Value; 
  myComputeThread.OnTerminate := GetResult; 
  myComputeThread.FreeOnTerminate := True; 
  myComputeThread.Start; 
end; 

On crée un thread de type TComputeThread en mode suspendu, c’est-à-dire qu’il n’exécutera pas sa propre procédure Execute tant que le thread appelant n’appellera pas la procédure Start.

On affecte la valeur IterMax et on associe l’évènement...

Thread transitoire par procédure anonyme (TThread.CreateAnonymousThread)

Des améliorations notables de syntaxe ont été apportées dans les dernières versions de Delphi XE. Maintenant, pour désynchroniser une action il n’est plus nécessaire de déclarer entièrement une nouvelle classe héritant de TThread. Dans le cas où l’on n’utilise cette désynchronisation qu’à un seul endroit et qu’il n’y a aucun code à mettre en commun, on peut utiliser le concept de « procédure anonyme ».

L’appel à la méthode CreateAnonymousThread raccourcit l’implémentation exposée précédemment. Le paramètre de la méthode est une procédure anonyme ne prenant aucun paramètre. Cependant, on pourra utiliser des variables de la procédure appelante sans problème.

Ainsi, on aura :

procedure TForm1.btnCalculClick(Sender: TObject); 
var AThread:TThread; 
   iterMax:integer; 
begin 
 // 
 iterMax := SpinEdit1.Value; 
 AThread := TThread.CreateAnonymousThread(procedure() 
 var iter:integer; 
   myRadian:Extended; 
   calculResult:double; 
   begin 
     iter := 0; 
     while (iter<iterMax)  do ...

Thread persistant

Un thread persistant est un thread dont la durée de vie est égale à celle de l’objet qui a instancié ce thread. Cela peut aller jusqu’à la durée de vie de l’application. On utilise en général ce genre d’implémentation quand il y a des tâches périodiques à exécuter avec le même type de contexte.

La destruction du thread devra être effectuée manuellement en utilisant les méthodes Terminate et WaitFor.

Voici en plusieurs étapes un exemple d’implémentation :

  • Étape 1 : Ajout dans la form d’une référence sur le thread pour que l’on puisse l’arrêter et/ou le détruire quand on le souhaite :

  TForm1 
 ... 
 private 
   { Private declarations } 
   FStart : Cardinal; 
   FCalculThread:TComputeThread; // <= référence sur le thread 
   function GetMaxIter:Extended; 
   procedure GetResult(Sender:TObject); 
 public 
  • Étape 2 : Ajout d’un évènement pour synchroniser l’affichage des résultats dans la classe TComputeThread :

type 
 TComputeThread  = class(TThread) 
 private 
   FResult:double; 
   FIterMax:Extended; 
   FLabel:TLabel; 
   FStart : Cardinal; 
   FOnComputingCompleted : TNotifyEvent; 
   procedure DoComputingEnd; 
 protected 
   procedure Execute;override;  
 public 
   constructor Create(Suspended:Boolean); ...

Les concepts de ressource partagée et de section critique

Dans un contexte multithread, il arrive très souvent qu’un objet soit référencé en même temps dans plusieurs threads, en général le thread appelant et le thread exécutant. C’est une ressource partagée.

Si les deux threads tentent d’accéder à cet objet en même temps, une exception de type EAccessViolation est levée. Pour éviter ce phénomène, on protégera l’accès à cette ressource partagée par une section critique.

La section critique est un objet TCriticalSection qui autorise lors de son utilisation l’exécution d’une portion de code à un seul thread et place en attente le ou les autres threads voulant exécuter cette portion de code.

Les méthodes utilisées pour protéger le code sont :

  • procedure Acquire; : indique le début de la portion de code à protéger.

  • procedure Release; : indique la fin de la portion de code à protéger.

Voici un exemple d’implémentation :

Dans notre cas, il suffit de protéger l’accès à la propriété par une section critique. Du côté de la classe TComputeThread, on doit donc :

1) Introduire un objet TCriticalSection dans la classe TComputeThread.

2) Créer un getter et un setter sur le champ FIterMax.

type 
 TComputeThread  = class(TThread) 
 private 
   FResult:double; 
   FIterMax:Extended; 
   FLabel:TLabel; 
   FStart : Cardinal; 
   FOnComputingCompleted : TNotifyEvent; 
   FLock : TCriticalSection; 
   procedure DoComputingEnd; 
   procedure SetIterMax(aValue:Extended); 
   function GetIterMax:Extended; 
 protected 
   procedure Execute;override;  
 
 public 
   destructor Destroy;override; 
   property IterMax : Extended read GetIterMAx write SetIterMax; 
   property Start : Cardinal read FStart; 
   property ComputeResult : double read FResult; 
   //ajout de l'évènement pour effectuer le rafraîchissement  
   property OnComputingCompleted : TNotifyEvent read 
FOnComputingCompleted write FOnComputingCompleted; 
end; 

Du côté de l’implémentation, les étapes suivantes sont définies :

  • Créer la section critique dans le constructeur de TComputeThread :

constructor TComputeThread....

Une liste thread safe, l’interface d’échange privilégiée entre deux threads

La manière la plus académique d’implémenter une application multithread est d’utiliser une interface d’échange pour séparer complètement les contextes d’exécution entre deux threads.

Avec notre exemple, on peut imaginer une classe modélisant un contexte d’exécution comme suit :

type 
 TExecutionContext= class(TObject) 
 public 
   IterMax:Extended; 
   CalculResult : double; 
end; 

Pour définir une liste d’échange thread safe, il existe deux possibilités décrites dans les sous-sections suivantes.

1. Par TCriticalSection

Nous avons vu précédemment qu’on peut protéger les ressources partagées par une section critique TCriticalSection.

Ainsi, une implémentation de cette liste d’échange serait simplement :

type 
 TThreadSafeList=class(TList<TExecutionContext>) 
 private 
   FLock : TCriticalSection; 
 public 
   procedure SafePush(anItem:TExecutionContext); 
   function SafePop:TExecutionContext; 
   constructor Create;reintroduce; 
   destructor Destroy;override; 
 end; 
 
 
 
{ TThreadSafeList } 
 
constructor TThreadSafeList.Create; 
begin 
 inherited...

PPL : la bibliothèque de programmation parallèle

Delphi propose une nouvelle interface ITask et quatre méthodes Start, FutureFor et Join pour tenter de couvrir tous les cas de désynchronisation de manière optimisée.

1. Pourquoi cette bibliothèque en plus de TThread ?

Delphi propose une implémentation de pool de thread déjà toute faite à travers cette librairie. Un pool de thread est un ensemble de thread qui est utilisé pour effectuer des tâches concurrentes. L’intérêt d’en maintenir plusieurs dans le groupe (pool) est que, si un thread est occupé à exécuter quelque chose, le gestionnaire du pool lui fournit un thread qui ne fait rien pour exécuter la tâche entrante. Implémenter une classe héritant de TThread revient à s’interfacer à l’API Windows CreateThread. Cependant, la création d’un thread pour effectuer une tâche n’est pas nécessairement synonyme de gain de performance ou d’expérience utilisateur, même si c’est le but recherché. De ce fait, suivant les cas d’application et les besoins de synchronisation avec le thread appelant, il est conseillé d’utiliser la bonne implémentation d’ITask pour utiliser le mécanisme de pool de thread intégré maintenant à...

Synchronisation de deux threads grâce à l’appel Future

Voici le cas d’application de l’utilisation de la méthode statique Future.

Un utilisateur effectue une action Action1. Action1 entraîne l’exécution d’une autre action Action2 dont le temps d’exécution est T secondes. De manière synchrone, si l’utilisateur veut le résultat de Action2, il doit attendre le thread principal pendant T secondes et bloque le thread appelant d’autant.

Avec l’utilisation de Future, l’exécution de Action2 est commencée immédiatement, mais c’est quand l’utilisateur demande le résultat de Action2 que le thread principal est bloqué. Ainsi, si l’utilisateur attend t secondes pour demander le résultat, le thread principal n’est bloqué que de T-t secondes. Cela permet d’éviter les blocages de rafraîchissement des applications.

images/10EP05.png

Pour mettre en évidence ce comportement, éditons la form ci-dessus avec l’ajout des éléments suivants :

  • Un TTimer et un TLabel pour afficher l’heure courante.

  • Deux TSpinEdit pour configurer un temps d’attente et un nombre quelconque. Ce nombre saisi sous le TLabel Result servira de résultat d’un calcul simulé. Ce calcul prend le nombre de secondes configuré dans le TSpinEdit en dessous...

Exécuter N actions en parallèle avec TParallel

1. Présentation

La classe TParallel permet d’exécuter N actions en parallèle et deux cas de figure se présentent. Soit l’utilisateur veut exécuter N fois la même action et il faudra alors utiliser la méthode statique TParallel.For, ou bien il s’agira d’exécuter différentes actions et on utilisera TParallel.Join.

2. Exécution de la même action en parallèle TParallel.For

Exemple

Dans N fichiers, on veut trouver le nombre de fois qu’un mot est contenu. Il s’agit bien d’un cas d’application de TParallel.For. On va demander d’exécuter pour N fichiers différents la recherche de la chaîne de caractères désirée.

L’implémentation se présente comme suit au niveau de la déclaration :

type TPositionResultList = class(TList<Int64>) 
  public 
    FileName:string; 
end; 
 
type 
 TForm2 = class(TForm) 
   btnSearch: TButton; 
   Memo1: TMemo; 
   Label1: TLabel; 
   procedure btnSearchClick(Sender: TObject); 
   procedure FormCreate(Sender: TObject); 
 private 
   { Private declarations } 
 public 
   { Public declarations } 
   FLog:TLog; 
   FLock:TCriticalsection; 
   FFileNameResultList: TObjectList<TPositionResultList>; 
   function SearchInFile(aResult:TPositionResultList; 
aPattern:string):boolean; 
 end; 
  • On déclare une classe TPositionResultList qui possède en tant que propriété le nom du fichier et qui hérite d’une liste d’entiers qui indique à quelle position dans le fichier se trouve la chaîne de caractères.

  • On déclare dans la Form un logger comme celui utilisé dans le chapitre sur les services pour tracer ce qui se passe.

  • On déclare une section critique TCriticalSection.

  • On déclare une liste de TPositionResultList qui sert à initialiser les retours des méthodes anonymes appelées par TParallel.For. Utiliser une liste déjà initialisée dans ce cas est un moyen d’éviter...

Recommandations

Les recommandations pour effectuer une bonne implémentation multithread sont les suivantes :

  • Bien découper l’application en rôles pour dissocier ce qui peut se désynchroniser ou non.

  • Bien protéger l’accès aux données partagées avec des sections critiques.

  • Quand des données sont partagées entre plus de deux threads, il est préférable d’utiliser une section critique globale pour éviter les phénomènes de dead lock.

Conclusion

Dans ce chapitre ont été parcourues les anciennes et nouvelles méthodes pour gérer l’asynchronisme. En général, c’est une réponse élégante à certains problèmes d’expérience utilisateur mais il peut en amener d’autres. Il n’existe pas de recette miracle et il est préférable de connaître toutes ces façons de programmer de l’asynchronisme pour pouvoir s’adapter à n’importe quelle situation. Tout simplement, cela permet également de lire du code écrit il y a quelques années quand Delphi ne bénéficiait pas de la PPL.