async / await en Delphi : sgcWebSockets sans gestionnaires d'événements | eSeGeCe Blog

async / await en Delphi : sgcWebSockets sans gestionnaires d'événements

· Composants

Jusqu'à présent, chaque appel asynchrone dans sgcWebSockets impliquait un gestionnaire d'événements. Vous appeliez Get sur un thread d'arrière-plan, ou vous définissiez Active := True et attendiez OnConnect. Le résultat arrivait ailleurs, dans une autre méthode, si bien que le code qui lançait l'opération et le code qui consommait la réponse vivaient à des endroits différents. Ajoutez une deuxième requête qui dépend de la première, plus la gestion des erreurs, plus un panneau « veuillez patienter » qu'il faut ensuite masquer, et vous vous retrouvez avec une petite machine à états éparpillée dans votre fiche : quelques champs privés pour vous rappeler ce que vous étiez en train de faire, un drapeau pour savoir si le dernier appel est toujours en cours, et un gestionnaire qui doit deviner laquelle des trois requêtes vient de revenir.

sgcWebSockets 2026.7 ajoute une API de style await qui supprime l'essentiel de tout cela. Une requête HTTP, une connexion WebSocket ou un appel de chat IA renvoient désormais un objet que vous pouvez attendre, auquel vous pouvez chaîner un rappel, ou que vous pouvez annuler, si bien que le code se lit de haut en bas dans l'ordre où il se produit. L'annulation n'est pas cosmétique non plus, elle interrompt l'opération sous-jacente au lieu de se contenter d'ignorer la réponse quand elle finit par arriver.

Tâches et futures

La nouvelle unité est sgcBase_AsyncAwait. Elle introduit deux interfaces. IsgcTask représente un travail qui se termine mais ne produit pas de valeur, et IsgcFuture<T> représente un travail qui produit une valeur de type T. Les deux vous offrent les quatre mêmes choses : Await pour bloquer jusqu'à ce que le travail soit terminé, ThenProc pour recevoir un rappel en cas de succès, OnError pour recevoir un rappel en cas d'exception, et Cancel pour l'arrêter.

Vous pouvez construire les vôtres avec TsgcAsync.Run. La procédure ou la fonction que vous passez s'exécute sur un thread d'arrière-plan, et les rappels reviennent sur le thread principal, il est donc sûr de toucher à l'interface utilisateur à l'intérieur de ThenProc et OnError.

uses
  sgcBase_AsyncAwait;

begin
  // the anonymous method runs on a background thread
  TsgcAsync.Run<Integer>(
    function: Integer
    begin
      Result := ExpensiveCalculation;
    end)
  .ThenProc(
    procedure(const Value: Integer)
    begin
      // main thread, safe to update the UI
      Label1.Caption := IntToStr(Value);
    end)
  .OnError(
    procedure(E: Exception)
    begin
      // main thread as well
      Label1.Caption := 'Failed: ' + E.Message;
    end);
end;

La tâche se maintient elle-même en vie pendant son exécution, un appel du type « lancer et oublier » comme celui ci-dessus est donc parfaitement valable. Si vous voulez l'inspecter plus tard, conservez l'interface dans une variable et lisez IsCompleted, IsCancelled ou State, qui vaut tsaPending, tsaRunning, tsaCompleted, tsaFailed ou tsaCancelled. Il existe aussi TsgcAsync.Delay(aMilliseconds), qui renvoie une tâche se terminant après un délai et pouvant être annulée avant son déclenchement, et TsgcAsync.RunOnMainThread, qui renvoie un morceau de code vers le thread principal depuis l'intérieur d'une tâche d'arrière-plan.

Attendre une requête HTTP

Le client HTTP expose directement des futures, vous n'avez donc pas à encapsuler l'appel vous-même. GetFuture et PostFuture renvoient un IsgcFuture<string> qui se résout en le corps de la réponse.

uses
  sgcHTTP_Client, sgcBase_AsyncAwait;

var
  oHTTP: TsgcHTTPClient;
  oFuture: IsgcFuture<string>;
begin
  oHTTP := TsgcHTTPClient.Create(nil);

  // the request runs on a background thread, this line returns immediately
  oFuture := oHTTP.GetFuture('https://httpbin.org/get');

  oFuture.ThenProc(
    procedure(const Value: string)
    begin
      // main thread: Value is the response body
      Memo1.Lines.Text := Value;
    end)
  .OnError(
    procedure(E: Exception)
    begin
      ShowMessage('GET failed: ' + E.Message);
    end);
end;

Si vous êtes déjà sur un thread de travail, ou si vous écrivez une application console, vous pouvez simplement bloquer et lire la valeur :

var
  vBody: string;
begin
  vBody := oHTTP.GetFuture('https://httpbin.org/get').Await;
  WriteLn(vBody);
end;

Await relève à nouveau l'exception levée par l'appel d'arrière-plan, enveloppée dans une EsgcAsyncException qui porte le nom de classe et le message d'origine, si bien qu'un simple try ... except autour fonctionne toujours. Si la future a été annulée, Await lève une EsgcTaskCancelled à la place.

Un avertissement qui compte plus que tout autre dans cet article. Appeler Await sur le thread principal bloque le thread principal, ce qui, dans une application VCL ou FMX, signifie une fenêtre gelée jusqu'au retour de l'appel. Dans une fiche, utilisez ThenProc. Réservez Await aux applications console, aux services, et au code qui s'exécute déjà sur un thread d'arrière-plan. Le client HTTP lui-même n'est pas thread-safe, donnez donc à chaque requête concurrente sa propre instance plutôt que d'en partager une entre plusieurs tâches.

Connecter un client WebSocket

Se connecter signifiait auparavant définir Active := True et attendre que OnConnect vous dise que cela avait fonctionné, ou que OnError vous dise le contraire. ConnectTask vous donne la même chose sous forme de tâche.

uses
  sgcWebSocket, sgcBase_AsyncAwait;

var
  oTask: IsgcTask;
begin
  sgcWebSocketClient1.Host := 'echo.esegece.com';
  sgcWebSocketClient1.Port := 443;
  sgcWebSocketClient1.TLS  := True;

  oTask := sgcWebSocketClient1.ConnectTask(10000); // 10 second timeout

  oTask.ThenProc(
    procedure
    begin
      // main thread: the connection is established and ready to send
      sgcWebSocketClient1.WriteData('hello');
    end)
  .OnError(
    procedure(E: Exception)
    begin
      ShowMessage('Could not connect: ' + E.Message);
    end);
end;

La tentative de connexion s'exécute sur un thread d'arrière-plan et le délai d'attente est un vrai délai d'attente. S'il expire, la tâche échoue au lieu de rester suspendue. Annuler la tâche déconnecte la socket, ce qui débloque une connexion encore en attente sur le réseau.

Attendre une connexion sans await : Connect et LastError

Tout n'a pas besoin d'une tâche. Parfois vous voulez simplement une ligne de code qui se connecte et vous dit si cela a fonctionné, ce que la plupart des gens attendaient de Active := True au départ. 2026.7 ajoute un Connect bloquant qui ne revient que lorsque la connexion est réellement établie, et pas seulement lancée.

var
  vError: string;
begin
  // returns True only when the WebSocket connection is fully established
  if sgcWebSocketClient1.Connect(10000) then
    sgcWebSocketClient1.WriteData('hello')
  else
    // find out why, without wiring an OnError handler
    ShowMessage('Connect failed: ' + sgcWebSocketClient1.LastError);

  // or get the reason back directly from the call
  if not sgcWebSocketClient1.Connect(vError, 10000) then
    ShowMessage('Connect failed: ' + vError);

  // and a Disconnect that waits until the socket is really closed
  sgcWebSocketClient1.Disconnect(5000);
end;

Cela referme une situation de concurrence qui mordait lors des reconnexions rapides. Comme Connect ne renvoie True qu'une fois la connexion pleinement établie, plus rien ne s'exécute contre une connexion à moitié prête, et Disconnect attend que la socket soit réellement fermée avant de rendre la main, si bien qu'une boucle de reconnexion ne peut pas chevaucher la connexion précédente. LastError vit sur le composant et contient la raison de l'échec de la dernière tentative, vous pouvez donc découvrir pourquoi une connexion a échoué sans vous abonner à un quelconque événement. Elle est effacée automatiquement au début de chaque tentative de connexion, et ClearLastError la réinitialise à la main.

Notez que Connect bloque le thread appelant, exactement comme le fait Await. Utilisez-le depuis un thread de travail ou une application console, et utilisez ConnectTask depuis une fiche.

Attendre un chat IA

Le client IA suit le même schéma. ChatAsync envoie l'invite sur un thread d'arrière-plan et renvoie un IsgcFuture<string> qui se résout en la réponse du modèle, si bien qu'un appel de chat dans une fiche tient désormais en trois lignes et sans aucun gestionnaire d'événements.

uses
  sgcAI_Chat, sgcBase_AsyncAwait;

var
  oFuture: IsgcFuture<string>;
begin
  oFuture := sgcAIChat1.ChatAsync('Summarize this text in one sentence.');

  oFuture.ThenProc(
    procedure(const Value: string)
    begin
      // main thread: Value is the model answer
      MemoAnswer.Lines.Text := Value;
      ButtonSend.Enabled := True;
    end)
  .OnError(
    procedure(E: Exception)
    begin
      MemoAnswer.Lines.Text := 'Error: ' + E.Message;
      ButtonSend.Enabled := True;
    end);

  ButtonSend.Enabled := False; // the UI stays responsive while it runs
end;

Annuler cette future interrompt la requête en cours, si bien qu'un utilisateur qui ferme la boîte de dialogue ou appuie sur Stop ne laisse pas un appel LLM tourner en arrière-plan et payer des jetons qu'il ne lira jamais.

Chaîner plutôt que bloquer (ThenProc / OnError)

Il vaut la peine d'être explicite sur le thread qui exécute quoi, car c'est là que le code de rappel se trompe habituellement. Le travail que vous passez à TsgcAsync.Run, et la requête derrière GetFuture, ConnectTask ou ChatAsync, s'exécutent tous sur un thread d'arrière-plan. Les rappels ThenProc et OnError sont remis en file sur le thread principal, il est donc sûr de toucher à une fiche à l'intérieur.

Un seul des deux se déclenche. ThenProc s'exécute lorsque l'opération réussit, OnError s'exécute lorsqu'elle lève une exception, et aucun des deux ne s'exécute si la tâche a été annulée. Cela suffit pour le cas séquentiel : lancez l'appel suivant depuis l'intérieur du ThenProc précédent et la chaîne se lit dans l'ordre où elle s'exécute. Si vous êtes déjà à l'intérieur d'une tâche d'arrière-plan et que vous devez toucher à l'interface utilisateur en cours de route, utilisez TsgcAsync.RunOnMainThread.

TsgcAsync.Run(
  procedure
  var
    oHTTP: TsgcHTTPClient;
    vBody: string;
  begin
    // background thread
    oHTTP := TsgcHTTPClient.Create(nil);
    try
      TsgcAsync.RunOnMainThread(
        procedure
        begin
          StatusBar1.SimpleText := 'Downloading...';
        end);

      vBody := oHTTP.Get('https://httpbin.org/get');

      TsgcAsync.RunOnMainThread(
        procedure
        begin
          // main thread again
          Memo1.Lines.Text := vBody;
          StatusBar1.SimpleText := 'Done';
        end);
    finally
      oHTTP.Free;
    end;
  end);

Une annulation qui annule vraiment

La plupart des implémentations d'« annulation » lèvent un drapeau et ignorent le résultat lorsqu'il finit par arriver. La socket reste ouverte, la requête continue de tourner, et le thread reste occupé. Dans sgcWebSockets, chaque appel asynchrone enregistre une action d'annulation auprès de la tâche, si bien que Cancel atteint l'opération sous-jacente : annuler une future HTTP interrompt la requête, annuler un ConnectTask déconnecte la socket, et annuler un ChatAsync interrompt l'appel IA. Le thread de travail bloqué se réveille au lieu de rester là jusqu'à l'expiration d'un délai d'attente.

Cela rend facile la construction d'un délai d'attente à partir d'une tâche Delay et d'une annulation, ce qui est le motif livré comme exemple 18 dans la bibliothèque :

var
  oRequest: IsgcFuture<string>;
  oTimeout: IsgcTask;
begin
  oRequest := oHTTP.GetFuture('https://httpbin.org/delay/10');
  oTimeout := TsgcAsync.Delay(3000);

  // if the request answers first, drop the timeout
  oRequest.ThenProc(
    procedure(const Value: string)
    begin
      oTimeout.Cancel;
      Memo1.Lines.Text := Value;
    end);

  // if the timeout fires first, abort the request for real
  oTimeout.ThenProc(
    procedure
    begin
      if not oRequest.IsCompleted then
      begin
        oRequest.Cancel;
        ShowMessage('Request timed out');
      end;
    end);
end;

Une tâche annulée ne déclenche ni ThenProc ni OnError. Son State devient tsaCancelled, IsCancelled renvoie True, et quiconque est bloqué dans Await reçoit une EsgcTaskCancelled. Si vous construisez votre propre tâche avec TsgcAsync.Run et qu'elle encapsule quelque chose d'interruptible, enregistrez votre propre interruption avec SetCancelProc et Cancel l'appellera.

Versions de Delphi prises en charge

L'unité async/await utilise les génériques et les méthodes anonymes, elle nécessite donc Delphi 2010 ou plus récent. Sur Delphi 7, 2007 et 2009, l'unité se compile en une unité vide et les méthodes asynchrones ne sont pas présentes, si bien que GetFuture, ConnectTask et ChatAsync ne sont tout simplement pas disponibles. Tout le reste de la bibliothèque prend toujours en charge Delphi 7 exactement comme avant, y compris l'API basée sur les événements à côté de laquelle ces méthodes se placent. Rien n'a été supprimé et aucun code existant ne change de comportement, les nouveaux appels sont des ajouts.

Les méthodes bloquantes Connect, Disconnect et la propriété LastError décrites plus haut ne sont pas génériques, elles sont donc disponibles sur toutes les versions de Delphi prises en charge.

Des exemples complets de tout cela sont livrés avec les sources dans sgcBase_AsyncAwait_Examples.pas, couvrant les tâches, les futures, les délais, l'annulation, l'interrogation de l'état, les requêtes HTTP parallèles et le chien de garde de délai d'attente ci-dessus.

Disponibilité

Les tâches et les futures sont disponibles dans sgcWebSockets 2026.7 de Delphi 2010 à Delphi 13 et sur C++Builder, sur Win32/Win64, Linux64, macOS, Android et iOS. Les méthodes bloquantes Connect, Disconnect et la propriété LastError sont disponibles de Delphi 7 à 13.

Les clients disposant d'un abonnement actif peuvent télécharger la nouvelle version depuis l'espace client. Les utilisateurs de la version d'essai peuvent récupérer l'installateur mis à jour sur esegece.com/products/websockets/download.

Des questions, des remarques, ou besoin d'aide pour porter une fiche basée sur les rappels vers les tâches ? Contactez-nous, vous recevrez une réponse des personnes qui ont écrit le code.