Utilisation de processus sous Delphi : fonctionnement de base.

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.


Mug Chinese New Year 2023 : year of the rabbitMug carte postale Sydney