Comme je l'indiquais dans la première partie de cette série sur les processus : il ne faut désormais bloquer les programmes en aucune manière, que ce soit en utilisation VCL pour Windows pur ou en FMX pour les autres cibles de compilation. Les systèmes d'exploitation n'aiment pas ça, les utilisateurs non plus.
Il faut donc passer les traitements longs ou interruptifs en processus secondaires. Cela oblige à une certaine gymnastique intellectuelle lorsqu'on conçoit nos programmes. Il ne faut en effet plus simplement penser "procédural" (à l'ancienne) ou "événementiel" (depuis Windows et Delphi 1, peut-être même Turbo Vision, j'avoue ne plus me souvenir trop de comment ça fonctionnait), mais "exécution parallèle".
La façon la plus simple pour créer un processus sous Delphi est cette construction :
tthread.CreateAnonymousThread(
procedure
begin
end).Start;
La méthode de classe TThread.CreateAnonymousThread permet de créer un processus anonyme qui exécute le contenu de la procédure passée en paramètre. La méthode retourne un objet TThread qu'il ne faut pas oublier de lancer, d'où le ".Start" final (que j'oublie encore régulièrement malgré l'expérience).
Ce qu'il faut avoir en tête, c'est que les éléments visuels de l'application ne sont disponibles que pour le processus principal. Toute action vers un composant visuel depuis un processus secondaire doit se faire en le synchronisant avec le processus principal. Il existe pour cela la méthode de classe TThread.Synchronize.
Je l'utilise en général sous la forme suivante :
tthread.Synchronize(nil,
procedure
begin
end);
Son premier paramètre correspond à une référence de thread qui peut par exemple être le processus en cours, si on en avait besoin ensuite.
tthread.Synchronize(tthread.CurrentThread,
procedure
begin
end);
Si on place un bouton sur une fiche et qu'on gère son événement onClick, voici ce que tout ceci pourrait donner.
procedure TForm1.Button1Click(Sender: TObject);
begin
// traitements habituels dans le processus principal
tthread.CreateAnonymousThread(
procedure
begin
// traitements dans le processus secondaire
tthread.Synchronize(nil,
procedure
begin
// interruption du processus secondaire pour exécuter ce code dans le processus principal
end);
// reprise du traitement secondaire
end).Start;
// traitements habituels dans le processus principal
end;
Notez que dans cet exemple le processus anonyme est lancé n'importe où dans le code du processus principal et peut s'exécuter au-delà de la fin d'exécution du clic sur le bouton. C'est tout l'intérêt des processus.
Pour aller plus loin et être opérationnel, il y a maintenant la question des paramètres et variables locales ou globales disponibles dans le processus secondaire.
Pour la faire court, les règles habituelles de visibilité des variables sont les mêmes en ce qui concerne les processus que pour les procédures et fonctions imbriquées les unes dans les autres. Une variable déclarée dans le onClick sera donc accessible par le processus même si l'exécution du onClick se termine. Le compilateur garde la zone mémoire active jusqu'à ce que tous les processus créés dans cet événement soient terminés.
Par extension de cette règle, on peut utiliser "self" dans un processus secondaire créé depuis une méthode d'objet. Il pointera sur l'instance de l'objet ayant entrainé sa création. Je ne le recommande pas pour des raisons de lisibilité du code et de maintenance, mais rien ne vous en empêche.
Attention cependant à un point très important : l'accès concurrent aux objets, variables, flux, fichiers et espaces mémoires en général. Si on a plusieurs processus modifiant la même chose le résultat final peut au mieux générer des erreurs ou au pire des choses incohérentes. Il ne faut pas hésiter à déclarer des MUTEX ou des sections critiques si nécessaire.
Pour finir je vous propose un cas pratique :
- créez un nouveau projet multi plateforme (donc Firemonkey, même si le principe serait le même en VCL)
- placez un tButton sur la fiche
- placez 5 tRectangle sur la fiche
- placez un tTimer sur la fiche
- éditez le onCreate de la fiche
- éditez le onClick du bouton
- éditez le onTimer du bouton
- copiez/collez ensuite le code suivant dans votre unité
- compilez et admirez le travail
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes,
System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Objects,
FMX.Controls.Presentation, FMX.StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Rectangle1: TRectangle;
Rectangle2: TRectangle;
Rectangle3: TRectangle;
Rectangle4: TRectangle;
Rectangle5: TRectangle;
Timer1: TTimer;
procedure Button1Click(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Déclarations privées }
procedure traitement(rectangle: TRectangle);
public
{ Déclarations publiques }
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
const
nb_secondes = 60;
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Enabled := false;
traitement(Rectangle1);
traitement(Rectangle2);
traitement(Rectangle3);
traitement(Rectangle4);
traitement(Rectangle5);
Timer1.Enabled := true;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Timer1.Interval := 1000;
Timer1.Tag := nb_secondes;
Timer1.Enabled := false;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
if (Timer1.Tag > 0) then
begin
Timer1.Tag := Timer1.Tag - 1;
Button1.Text := Timer1.Tag.ToString;
end
else
Timer1.Enabled := false;
end;
procedure TForm1.traitement(rectangle: TRectangle);
begin
tthread.CreateAnonymousThread(
procedure
var
couleur: talphacolor;
i: integer;
begin
for i := 1 to nb_secondes * 10 do
begin
case random(8) of
0:
couleur := talphacolors.red;
1:
couleur := talphacolors.orange;
2:
couleur := talphacolors.yellow;
3:
couleur := talphacolors.green;
4:
couleur := talphacolors.blue;
5:
couleur := talphacolors.Violet;
6:
couleur := talphacolors.Pink;
7:
couleur := talphacolors.white;
else
couleur := talphacolors.black;
end;
tthread.Synchronize(nil,
procedure
begin
rectangle.Fill.Color := couleur;
end);
sleep(100); // attente de 0,1 seconde
end;
end).Start;
end;
end.
Ce qui donne ceci (en réduisant la durée à 10 secondes) :
La prochaine fois nous verrons comment lancer des processus bloquants sans l'être vraiment, ce qui peut servir lors de la récupération de données provenant d'API en ligne pour l'affichage à l'écran.