Gestion des processus sous Windows
Gestion des processus sous Windows
Les processus
Un processus désigne une application en cours d’exécution. Il y a autant de processus actifs que d’application en cours
d’exécution. Et si on lance trois fois de suite une même application, Windows créée trois processus. Pour ceux qui ont
connu la programmation sous Windows 3.x, la notion de processus correspond à la notion d’instance..
Pour Windows, les processus s’exécutent dans des espaces mémoires indépendants. Un processus ne peut accéder à la zone
mémoire d’un autre processus, même si il s’agit de deux processus d’une même application.
Un processus sous Windows est caractérisé par deux valeurs : un Handle (de type HANDLE) et un identifiant(de type
DWORD). Le Handle permet à Windows d’envoyer des messages au processus (par exemple les messages de rafraîchissement
de l’affichage, les messages de la souris….). L’identifiant permet à Windows d’atteindre un processus (par exemple pour
fermer un processus récalcitrant par l’intermédiaire du gestionnaire de tâches.)
Cependant, si ShellExecute permet de créer un processus, un fois que le processus est créé, on ne peut plus intervenir
dessus. Par exemple il peut être utile de guetter la fin du processus. Pour un usage plus précis des processus, il est
préférable d’utiliser la fonction CreateProcess.
La déclaration de CreateProcess est :
function CreateProcess(lpApplicationName : PChar, lpCommandeline : PChar, lpProcessAttributes :
LPSECURITY_ATTRIBUTES, lpThreadAttributes : LPSECURITY_ATTRIBUTES, bInheritHandles : boolean,
dwCreationsFlags : DWORD, lpEnvironnement : Pointer, lpCurrentDirectory : PChar, lpStartUpInfo :
STARTUPINFO, lpProcessInformation : PROCESS_INFORMATION) : Boolean
lpApplicationName est le nom de l’application à ouvrir. Si l’application ne se trouve pas dans le répertoire de Windows,
ou dans les répertoires donnés par les variablement d’environnement, il faut préciser le chemin complet.
lpCommandline correspond à la liste de paramètre à fournir au programme. Si lpApplicationName est égal à nil, alors le
premier paramètre est pris comme nom d’application.
lpProcessAttributes est la définition des paramètres de sécurité pour le nouveau processus. En laissant lpProcessAttibutes
à nil, le processus récupère les paramètres de sécurité par défaut.
lpThreadAttributes est identique à lpProcessAttributes mais s’applique pour le cas du thread principal. Voir le paragraphe
sur les threads ci dessous.
bInheritHandles indique si le processus enfant peut utiliser les handles de ses parent
dwCreationFlags est un drapeau qui indique le mode de création du processus enfant.
lpEnvironnement est un pointeur vers les paramètres d’environnement du nouveaux processus (PATH…). Si
lpEnvironnement est à nil, alors le processus enfant récupère les variables d’environnement du processus père.
lpCurrentDirectory : chemin du répertoire courant du nouveau processus. Si lpCurrentDirectory est à nil, alors le processus
enfant récupère le répertoire courant du processus père.
lpStartInfo : est un record qui défini le mode d’affichage de la fenêtre du nouveau processus.
lpProcessInfo est un record qui est renseigné par l’appel à la fonction CreateProcess. C’est par lpProcessInfo que l’on
récupère les information concernant le Handle et l’ID du processus un fois qu’il est crée.
Pour créer un processus on fait donc :
var
Si : STARTUPINFO;
Pi : PROCESS_INFORMATION;
begin
ZeroMemory(@si,sizeof(STARTUPINFO));
[Link]:=STARTF_USESHOWWINDOW;
[Link]:=SW_SHOWNORMAL;
CreateProcess(nil,'[Link]',nil,nil,True,0,nil,nil,Si,Pi);
end ;
WaitForSingleObject([Link],INFINITE);
MessageBox(handle,'c''est fini','Fin',MB_OK);
Les threads
Windows est un système multitâche. Cela signifie qu’il peut exécuter plusieurs actions en même temps. Par exemple il
n’est pas rare de travailler sur un tableur pendant que le logiciel de messagerie interroge à intervalles réguliers le contenu
de la boite e-mail, en même temps qu’un antivirus surveille l’activité de toutes les applications en cours de fonctionnement.
Dans le paragraphe précédent, il a été question de processus. En développant l’analyse, on pourrait penser que un processus
égal une tâche. En fait il n’en est rien. Un processus contient au minimum une tâche, mais il peut très bien en contenir
plusieurs. Implicitement, lorsqu’on démarre un processus, on crée en même temps la tâche principale de ce processus. On
peut en déduire qu’un processus est une sorte de conteneur de tâches. Les tâches sont communément appelées « thread »
dans la terminologie Windows. Et lorsqu’on parle de multitâche, on emploiera le terme de « multi-threading ».
unit UnitMyThread_1;
interface
uses
Classes;
type
MyThread_1 = class(TThread)
private
{ Déclarations privées }
implementation
{ Important : les méthodes et propriétés des objets de la VCL ou CLX peuvent uniquement être
utilisées dans une méthode appelée avec Synchronize. Par exemple,
Synchronize(UpdateCaption);
procedure MyThread_1.UpdateCaption;
begin
[Link] := 'Updated in a thread';
end; }
{ MyThread_1 }
procedure MyThread_1.Execute;
begin
{ Placez le code du thread ici }
end;
end.
Le code que l’on souhaite donc exécuter au sein du thread sera donc placé (ou tout du moins appelé) dans la méthode
Execute de la classe.
La classe ancêtre TThread fournit les propriétés de bases :
property FreeOnTerminate : Boolean
Si FreeOnTerminate vaut True, alors l’objet thread est détruit lorsqu’il a terminé son exécution. Sinon on doit libérer le
thread en appelant la méthode Free.
Indique le niveau de priorité du thread. Priority peu prendre les valeurs suivantes :
§ tpIdle : le niveau de priorité le plus bas. Le thread guette le moment où Windows ne fait rien.
§ tpLowest : contrairement à ce que son nom indique, ce n’est pas le niveau de priorité le plus bas, mais seulement le plus
bas parmi les threads systématiquement exécutés (contrairement à tpIdle).
§ tpLower : priorité légèrement plus faible que la normale.
§ tpNormal : priorité standard.
§ tpHigher : priorité légèrement plus élevée que la normale
§ tpHighest : priorité la plus élevée ne bloquant les autres threads
§ tpTimeCritical : priorité la plus élevée, pouvant bloquer les autres threads.
Si CreateSuspended vaut True, alors le thread est initialisé mais sont éxécution est stoppée. Sinon le thread s’exécute dès
qu’il est créé. Pour redémarré un thread qui est suspendu, il suffit de passer sa propriété Suspended à False
Auteur : Laurent Berne Mise en page : Céline Cantat
[Link]@[Link] [Link]@[Link] 4
Pour commencer il faut donc définir la procédure Execute. On peut commencer avec une simple boîte de dialogue « Hello
Word » :
procedure MyThread_1.Execute;
begin
MessageBox(0,’Hello Word !’, ‘Bonjour du Thread’, MB_OK);
end;
Maintenant il faut utiliser le thread dans le projet. Dans la fiche qui déclenche le thread, il faut ajouter l’unité qui contient
le thread dans la clause Uses. Ensuite, on créer un objet thread en appelant son constructeur
MyThread_1:=TMyThread_2.Create(False);
end;
Lors de l’exécution de la procédure Button1Click, une boîte de dialogue apparaît. Bien sur ce petit exemple n’a pas
vraiment d’utilité. Son seul but est de montrer l’insertion d’un thread dans le code.
Pour aller plus loin dans l’étude des threads, nous allons étudier l’implémentation d’un compteur dans un thread . Le
principe : afficher dans un label le contenu d’une variable qui qui s’incrémente toutes les secondes. Par principe, les
threads sont des espaces clos. Il faut donc lui transmettre le TLabel dans lequel le compteur va s’afficher. Dans ce but, il
faut faire une surcharge du constructeur :
Voici donc la déclaration du nouvel objet thread : MyThread_2 , créé par la méthode décrite ci dessus..:
unit UnitMyThread_2;
interface
uses
Classes,StdCtrls,SysUtils,Windows;
type
TMyThread_2 = class(TThread)
private
{ Déclarations privées }
Display : TLabel;
protected
procedure Execute; override;
public
constructor Create(ADisplay : TLabel);
end;
Dans cette implémentation on distingue l’appelle au constructeur originel hérité de la classe TThread par :
inherited Create(False);
procedure TMyThread_2.Execute;
var
Count : integer;
begin
{ Placez le code du thread ici }
Count :=0;
Repeat
Sleep(1000);
Inc(Count);
[Link]:= IntToStr(Count);
until Count >30;
end;
Dans le projet, sur la forme principal il faut placer un objet TLabel. Pour activer le thread on procède ainsi :
MyThread_2:=TMyThread_2.Create(Label1);
end;
A l’exécution, le contenu du label est modifié toutes les secondes pour afficher le résultat du compteur. Pour avoir un
aperçu du principe du mutlti-threading, il faut rajouter un deuxième TLabel sur la fiche. Ensuite il faut créer un deuxième
objet thread et l’activer indépendamment du premier :
MyThread_2b:=TMyThread_2.Create(Label2);
end;
A l’exécution, les deux threads sont crées indépendamment par l’utilisateur. L’affichage renvoyé indique bien que les deux
threads s’exécutent de manière totalement indépendante.
Dans l’unité qui contien le thread, il faut bien sûr rajouter dans la clause uses l’unité contenant la fiche. Dans l’objet thread,
on rajoute une variable privée donnant accés au contenu de la fiche. La déclaration du thread devient donc :
unit UnitMyThread_3;
interface
uses
Classes,SysUtils,UnitProgressForm;
type
TMyThread_3 = class(TThread)
private
{ Déclarations privées }
FProgressForm : FTProgressForm;
protected
procedure Execute; override;
public
constructor Create(Suspended : Boolean);
end;
FProgressForm := [Link](nil);
[Link];
...OnTerminate := OnTerminateProcedure;
end;
procedure TMyThread_3.Execute;
var
Count : integer;
begin
Count :=0;
Repeat
Sleep(100);
Inc(Count);
if Assigned(FProgressForm) then
[Link]:=Count;
Chaque fois que l’on accède à la fiche, on teste si elle est correctement initialisée avec la fonction Assign. A la fin du
thread, on libère la fiche avec l’appel à [Link]. La procédure OnTerminateProcedure est
appelée juste avant la destruction de l’objet car dans le constructeur elle a été affecté à l’évenement OnTerminate par
la ligne :
OnTerminate := OnTerminateProcedure;
interface
uses
Classes,SysUtils;
type
TMyThread_4 = class(TThread)
private
{ Déclarations privées }
FProgressForm : FTProgressForm;
procedure OnTerminateProcedure(Sender : TObject);
protected
procedure Execute; override;
public
ThreadTerminated : Boolean;
constructor Create(Suspended : Boolean);
end;
Repeat
// boucle vide pour attendre la fin du thread
[Link];
until MyThread_4.ThreadTerminated;
MessageBox(Handle,'Le thread est terminé','Avertissement',MB_OK);
end;
La boucle Repeat .. Until va tester en permanence la variable ThreadTerminated qui a été rajoutée. A
l’exécution, la boîte de dialogue apparaîtra lorsque le thread aura terminé son éxécution
Repeat
[Link];
MyThread_4.Suspended := [Link];
until MyThread_4.ThreadTerminated;
MessageBox(Handle,'Le thread est terminé','Avertissement',MB_OK);
end;
A l’exécution, le thread se mettra en pause à chaque fois que l’on cochera la case à cocher, et se remettra en route dès
qu’on la décochera.
unit UnitMyThread_5;
interface
uses
Classes,SysUtils,UnitProgressForm, Windows;
type
TMyThread_5 = class(TThread)
private
{ Déclarations privées }
FProgressForm : TProgressForm;
FName : string;
procedure OnTerminateProcedure(Sender : TObject);
protected
procedure Execute; override;
public
ThreadTerminated : Boolean;
end;
procedure TMyThread_5.Execute;
var
Count : integer;
begin
Count :=0;
Repeat
Inc(Count);
if Assigned(FProgressForm) then
[Link]:=Count div 1000;
end;
La procédure Execute ne fait pas appel à la fonction Sleep. Elle utilise donc tout le temps de calcul dont elle dispose
Enfin la nouvelle procédure OnTerminateProcedure :
Lorsque le thread est terminé, une boite de dialogue va s’ouvrir pour indiquer que le nom du thread qui se termine.
Dans le projet :
Durant l’exécution , le thread MyThread_Faible est lancé avant le thread MyThread_Prioritaire. Pourtant
MyThread_Prioritaire se termine avant MyThread_Faible.
Les Mutex
Ce sont des objets de synchronisation que se partage les threads en cours d’exécution. Leur nom vient de la contraction
de « MUTual EXclusive objects ». Pour faire une analogie simple : un mutex est le témoin que se passe les coureurs dans
une course de relais, les coureurs étant les threads. Le premier coureur démarre, emportant le témoin avec lui. A un certain
moment de sa course, le deuxième coureur démarre lui aussi mais modère son allure (voir s’arrête ??) tant que le premier
coureur ne lui a pas passer le témoin.
Dans le cas des threads, un thread démarre, bloquant l’usage du mutex. Plus tard dans le code, un deuxième thread
démarre et bloque sa propre exécution en attendant que le premier thread libère le mutex. Ce deuxième thread est au
courant de l’état du mutex en interrogeant l’API WaitForSingleObject.. Une fois le mutex libéré, le deuxième thread s’en
«empare» et poursuit son exécution.
Dans la terminologie, on dit qu’un mutex est à l’état «non signalé» lorsque un thread bloque son usage et qu’il est à l’état
«signalé» si il est libre.
Pour instancier un nouvel objet mutex, on fait appel à l’API Create Mutex :
function CreateMutex(lpMutexAttributes : LPSECURITY_ATTRIBUTES ; bInitialState : Boolean ; lpName :
PChar) : THandle ;
lpMutexAttributes est la définition des paramètres de sécurité pour le nouveau processus. En laissant lpMutexAttributes à
nil, le processus récupère les paramètres de sécurité par défaut.
bInitialState indique si le thread est signalé (True) ou non.
lpName est le nom du mutex..
dwDesiredAccess est de type DWORD (c’est à dire LONGWORD). il peut prendre deux valeurs :
§ MUTEX_ALL_ACCESS : permet tous les accès possible à l’objet mutex
§ SYNCHRONIZE : pour Windows NT/2000/XP seulement : permet d’utiliser le Handle dans une fonction d’attente.
bInheritHandle détermine si les processus enfants peuvent hériter ou non.
lpName : Nom du mutex recherché.
Retourne le handle du mutex recherché
Pour attendre que le mutex soit disponible il faut utiliser l’API WaitForSingleObject. Cette fonction de l’API bloque
Une fois que l’on a plus besoin du mutex, il faut le libérer pour que d’autres threads puissent le récupérer. On utilise l’API
ReleaseMutex :
Note : pour synchroniser un thread avec plusieurs autres simultanément, on préférera la foncion WaitForMutlipleObjects
et on associera un mutex différent pour chaque thread synchronisé.
Enfin il ne faut pas oublier de libérer le mutex une fois que tous les threads l’on utilisé. On utilise CloseHandle :
function CloseHandle(hObject : THandle) : Boolean;
Pour appliquer tout cela, nous allons faire un thread qui monopolise le mutex le temps d’afficher une boite de dialogue.
La déclaration :
unit UnitMyThread_6;
interface
uses
Classes,Windows;
type
TMyThread_6 = class(TThread)
private
{ Déclarations privées }
FMutex : THandle;
FName : string;
protected
procedure Execute; override;
public
constructor Create(Suspended : Boolean; Name : string);
end;
Le constructeur :
end;
procedure TMyThread_6.Execute;
var
MyMessage : string;
begin
{ Placez le code du thread ici }
WaitForSingleObject(FMutex,INFINITE);
MyMessage := 'Le thread '+ FName +' vient de récupérer le mutex';
MessageBox(0,PChar(MyMessage),'Avertissement',MB_OK);
ReleaseMutex(FMutex);
WaitForSingleObject(FMutex,INFINITE);
MyMessage := 'Le thread '+ FName +' vient de récupérer une deuxième fois le mutex';
MessageBox(0,PChar(MyMessage),'Avertissement',MB_OK);
ReleaseMutex(FMutex);
WaitForSingleObject(FMutex,INFINITE);
MyMessage := 'Le thread '+ FName +' vient de récupérer une dernière fois le mutex';
MessageBox(0,PChar(MyMessage),'Avertissement',MB_OK);
ReleaseMutex(FMutex);
end;
end;
Lors de l’exécution, deux instances du même thread sont appelées. Chacune d’elles capture puis relâche le mutex, à
plusieurs reprises, ce qui donne cet effet d’alternance (une fois Thread_1, une fois Thread_2..)
Les sémaphores
Le principe des sémaphores est de gérer des accès simultanés à des ressources. Par exemple on peut très bien imaginer que
trois threads essaient d’écrire dans un fichier. Si on laisse les trois threads sans synchronisation, il y aura des violations.
Les sémaphores décomptent les accés aux ressources et font patienter les threads. Les threads, lors de leur execution,
interrogent le sémaphore par l’intermédiaire de l’API WaitForSingleObject. Ils sont bloqués tant que le sémaphore ne les
autorise pas à continuer. Pour cela, le sémaphore vérifie le nombre de threads accédant à la ressource, et si il reste «de la
place disponible», envoie le signal qui débloquera les threads appelant.
lpSemaphoreAttributes est la définition des paramètres de sécurité pour le nouveau processus. En laissant
lpSemaphoreAttribute à nil, le processus récupère les paramètres de sécurité par défaut.
lIntialCount est le nombre de resources initialement dipsonibles (doit être supérieur à 0)
lMaximumCount est le nombre total de ressources disponibles.
dwDesiredAccess est de type DWORD (c’est à dire LONGWORD). il peut prendre deux valeurs :
§ SEMAPHORE_ALL_ACCESS : permet tous les accès possible à l’objet mutex
§ SEMAPHORE_MODIFY_STATE : autorise le décompte du sémaphore lors du relâchement.
§ SYNCHRONIZE : pour Windows NT/2000/XP seulement : permet d’utiliser le Handle dans une fonction d’attente.
bInheritHandle détermine si les processus enfants peuvent hériter ou non.
lpName : Nom du sémaphore recherché.
Retourne le handle du sémaphore recherché
Pour la mise en application la ressource sera symbolisée par un TProgressBar propriétaire d’une fiche externe au
thread(figure 19.2)
Afin de simuler l’appel d’une ressources exclusives, on définit à cette fiche une méthode InvokeDisplay :
unit UnitMyThread_7;
interface
uses
Classes,ComCtrls,SysUtils,Windows,UnitSemaphoreForm;
TMyThread_7 = class(TThread)
private
{ Déclarations privées }
FSemaphore : THandle;
FSemaphoreForm : TSemaphoreForm;
FName : string;
protected
procedure Execute; override;
public
ProgressBar : TProgressBar;
constructor Create(Suspended : Boolean; Name : string ; SemaphoreForm : TSemaphoreForm);
end;
On déclare la constante SEMAPHORE_ALL_ACCESS car elle n’est pas délcarée dans la source [Link]. Ainsi on
est en accord avec le SDK Win32.
Le constructeur du Thread :
La procédure Execute :
procedure TMyThread_7.Execute;
var
Count : integer;
begin
Count :=0;
WaitForSingleObject(FSemaphore,INFINITE);
ProgressBar := [Link](FName);
Repeat
Inc(Count);
if Assigned(ProgressBar) then
[Link] :=Count div 1000;
until Count >100000;
ReleaseSemaphore(FSemaphore,1,nil);
end;
Et enfin dans l’application on créée trois instance de ce thread qui vont invoquer la barre de progression.
Thread_1 := TMyThread_7.Create(False,'Thread_1',SemaphoreForm);
Thread_2 := TMyThread_7.Create(False,'Thread_2',SemaphoreForm);
Thread_3 := TMyThread_7.Create(False,'Thread_3',SemaphoreForm);
CloseHandle(Semaphore);
end;
Les Evènements
C’est le dernier objet de synchronisation disponible. Un évènement, c’est un peu comme le signal de départ lors d’une
course. Il existe deux styles d’évènement. Le premier, classique, indique que tous les threads qui attendent cet évènement
peuvent démarrer. Le deuxième lui donne le départ à un seul et unique thread. Les autres attendent que l’évènement leur
autorise de démarrer. Ca pourrait s’apparenter au départ d’une course contre la montre du cyclisme.
Lors de l’exécution, les threads attendent que l’évènement se déclenche. Ensuite, c’est le
Delphi dispose d’un objet permettant d’utiliser simplement les évenements : l’objet TEvent.
Ses propriétés sont :
property Handle: THandle;
En lecture seule, le handle de l’événement.
EventAttributes est la définition des paramètres de sécurité pour le nouveau processus. En laissant lpSemaphoreAttribute
à nil, le processus récupère les paramètres de sécurité par défaut.
ManualReset indique si l’événement reste actif une fois déclenché ou si il revient à l’état inactif.
IntialState : indique si l’événement est déclenché.
Name : nom de l’événement, nécessaire pour la synchronisation.
L’objet dispose de méthode permettant au thread qui le possède d’attendre l’événement (procedure WaitFor), de le
déclencher (procédure SetEvent) ou de le réinitialisé (procedure ReseEvent).
Pour la mise en application, l’événement sera déclenché par un clic sur un bouton.
on place le code suivant dans l’unité du thread :
unit UnitMyThread_8;
interface
uses
Classes,SyncObjs,Windows;
type
TMyThread_8 = class(TThread)
private
{ Déclarations privées }
FEvent : TEvent;
FName : string;
protected
procedure Execute; override;
Auteur : Laurent Berne Mise en page : Céline Cantat
[Link]@[Link] [Link]@[Link] 16
public
constructor Create(Suspended : Boolean; Name : string);
end;
implementation
FEvent :=[Link](nil,False,False,'MyEvent');
FName := Name;
end;
procedure TMyThread_8.Execute;
var
MyMessage : string;
begin
{ Placez le code du thread ici }
[Link](INFINITE);
MyMessage := 'Le thread '+ FName +' vient d''être déclenché';
MessageBox(0,PChar(MyMessage),'Avertissement',MB_OK);
end;
end.
Thread_1 : TMyThread_8;
Thread_2 : TMyThread_8;
Thread_3 : TMyThread_8;
begin
Thread_1 := TMyThread_8.Create(False,'Thread_1');
Thread_2 := TMyThread_8.Create(False,'Thread_2');
Thread_3 := TMyThread_8.Create(False,'Thread_3');
end;
Lors de l’éxecution, on créée les trois threads ; qui vont attendre l’évènement. Comme l’événement se réinitialise
automatiquement, seul un thread à la fois va se poursuivre.