async / await in Delphi: sgcWebSockets senza gestori di eventi | Blog eSeGeCe

async / await in Delphi: sgcWebSockets senza gestori di eventi

· Componenti

Fino ad ora, ogni chiamata asincrona in sgcWebSockets significava un gestore di eventi. Chiamavi Get su un thread in background, oppure impostavi Active := True e aspettavi OnConnect. Il risultato arrivava altrove, in un altro metodo, quindi il codice che avviava l'operazione e il codice che consumava la risposta vivevano in posti diversi. Aggiungi una seconda richiesta che dipende dalla prima, più la gestione degli errori, più un pannello "attendere prego" che deve essere nascosto di nuovo, e ti ritrovi con una piccola macchina a stati sparsa per la tua form: un paio di campi privati per ricordare cosa stavi facendo, un flag per sapere se l'ultima chiamata è ancora in corso e un gestore che deve capire quale delle tre richieste è appena tornata.

sgcWebSockets 2026.7 aggiunge un'API in stile await che elimina gran parte di tutto questo. Una richiesta HTTP, una connessione WebSocket o una chiamata di chat AI ora restituiscono un oggetto che puoi attendere, a cui puoi concatenare una callback o che puoi annullare, così il codice si legge dall'alto in basso nell'ordine in cui accade. Nemmeno la cancellazione è cosmetica, interrompe l'operazione sottostante invece di limitarsi a ignorare la risposta quando finalmente arriva.

Task e future

La nuova unit è sgcBase_AsyncAwait. Introduce due interfacce. IsgcTask rappresenta un lavoro che termina ma non produce un valore, e IsgcFuture<T> rappresenta un lavoro che produce un valore di tipo T. Entrambe ti offrono le stesse quattro cose: Await per bloccarti finché il lavoro non è finito, ThenProc per ottenere una callback quando ha successo, OnError per ottenere una callback quando solleva un'eccezione e Cancel per fermarlo.

Puoi crearne di tuoi con TsgcAsync.Run. La procedura o la funzione che passi viene eseguita su un thread in background, e le callback tornano sul thread principale, quindi è sicuro toccare la UI dentro ThenProc e 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;

Il task si mantiene in vita mentre viene eseguito, quindi una chiamata fire-and-forget come quella qui sopra va benissimo. Se vuoi ispezionarlo in seguito, conserva l'interfaccia in una variabile e leggi IsCompleted, IsCancelled o State, che assume uno tra i valori tsaPending, tsaRunning, tsaCompleted, tsaFailed o tsaCancelled. C'è anche TsgcAsync.Delay(aMilliseconds), che restituisce un task che si completa dopo un timeout e può essere annullato prima che scatti, e TsgcAsync.RunOnMainThread, che rimanda un pezzo di codice al thread principale dall'interno di un task in background.

Attendere una richiesta HTTP

Il client HTTP espone direttamente le future, quindi non devi incapsulare tu stesso la chiamata. GetFuture e PostFuture restituiscono una IsgcFuture<string> che si risolve nel corpo della risposta.

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;

Se sei già su un thread di lavoro, o stai scrivendo un'applicazione console, puoi semplicemente bloccarti e leggere il valore:

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

Await risolleva qualunque eccezione abbia sollevato la chiamata in background, incapsulata in una EsgcAsyncException che porta con sé il nome della classe originale e il messaggio, quindi un semplice try ... except intorno funziona ancora. Se la future è stata annullata, Await solleva invece EsgcTaskCancelled.

Un avvertimento che conta più di ogni altro in questo articolo. Chiamare Await sul thread principale blocca il thread principale, il che in un'applicazione VCL o FMX significa una finestra congelata finché la chiamata non ritorna. In una form, usa ThenProc. Riserva Await alle applicazioni console, ai servizi e al codice che è già in esecuzione su un thread in background. Il client HTTP stesso non è thread-safe, quindi assegna a ogni richiesta concorrente la propria istanza invece di condividerne una tra i task.

Connettere un client WebSocket

Connettersi significava impostare Active := True e aspettare che OnConnect ti dicesse che aveva funzionato, oppure che OnError ti dicesse di no. ConnectTask ti offre la stessa cosa sotto forma di task.

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;

Il tentativo di connessione viene eseguito su un thread in background e il timeout è un timeout vero. Se scade, il task fallisce invece di restare appeso. Annullare il task disconnette il socket, il che sblocca una connessione che sta ancora aspettando la rete.

Aspettare una connessione senza await: Connect e LastError

Non tutto ha bisogno di un task. A volte vuoi solo una riga di codice che si connetta e ti dica se ha funzionato, che è quello che la maggior parte delle persone si aspettava facesse Active := True fin dall'inizio. La 2026.7 aggiunge un Connect bloccante che ritorna solo quando la connessione è davvero stabilita, non semplicemente avviata.

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;

Questo chiude una race condition che colpiva durante le riconnessioni rapide. Poiché Connect restituisce True solo quando la connessione è completamente attiva, non viene più eseguito nulla contro una connessione a metà, e Disconnect aspetta che il socket sia davvero chiuso prima di ritornare, così un ciclo di riconnessione non può sovrapporsi alla connessione precedente. LastError risiede sul componente e contiene il motivo per cui l'ultimo tentativo è fallito, così puoi scoprire perché una connessione è fallita senza iscriverti ad alcun evento. Viene azzerato automaticamente all'inizio di ogni tentativo di connessione, e ClearLastError lo reimposta manualmente.

Nota che Connect blocca il thread chiamante, esattamente come fa Await. Usalo da un thread di lavoro o da un'applicazione console, e usa ConnectTask da una form.

Attendere una chat AI

Il client AI segue lo stesso schema. ChatAsync invia il prompt su un thread in background e restituisce una IsgcFuture<string> che si risolve nella risposta del modello, quindi una chiamata di chat in una form ora sono tre righe e nessun gestore di eventi.

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;

Annullare quella future interrompe la richiesta in corso, così un utente che chiude la finestra di dialogo o preme Stop non lascia una chiamata LLM in esecuzione in background a pagare token che non leggerà mai.

Concatenare invece di bloccare (ThenProc / OnError)

Vale la pena essere espliciti su quale thread esegue cosa, perché è lì che il codice delle callback di solito va storto. Il lavoro che passi a TsgcAsync.Run, e la richiesta dietro GetFuture, ConnectTask o ChatAsync, vengono eseguiti tutti su un thread in background. Le callback ThenProc e OnError vengono accodate di nuovo sul thread principale, quindi toccare una form al loro interno è sicuro.

Solo una delle due scatta. ThenProc viene eseguita quando l'operazione ha successo, OnError viene eseguita quando solleva un'eccezione, e nessuna delle due viene eseguita se il task è stato annullato. Questo basta per il caso sequenziale: avvia la chiamata successiva dall'interno della ThenProc precedente e la catena si legge nell'ordine in cui viene eseguita. Se sei già dentro un task in background e devi toccare la UI a metà strada, usa 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);

Una cancellazione che annulla davvero

La maggior parte delle implementazioni di "annulla" imposta un flag e ignora il risultato quando alla fine arriva. Il socket resta aperto, la richiesta continua a essere eseguita e il thread resta occupato. In sgcWebSockets ogni chiamata asincrona registra un'azione di annullamento sul task, così Cancel raggiunge l'operazione sottostante: annullare una future HTTP interrompe la richiesta, annullare un ConnectTask disconnette il socket, e annullare un ChatAsync interrompe la chiamata AI. Il thread di lavoro bloccato si sveglia invece di restare lì fino allo scadere di un timeout.

Questo rende facile costruire un timeout a partire da un task Delay e da un annullamento, che è lo schema fornito come esempio 18 nella libreria:

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;

Un task annullato non fa scattare né ThenProcOnError. Il suo State diventa tsaCancelled, IsCancelled restituisce True, e chiunque sia bloccato in Await riceve una EsgcTaskCancelled. Se costruisci un tuo task con TsgcAsync.Run e questo incapsula qualcosa di interrompibile, registra la tua procedura di interruzione con SetCancelProc e Cancel la chiamerà.

Versioni di Delphi supportate

La unit async/await usa i generics e i metodi anonimi, quindi richiede Delphi 2010 o superiore. Su Delphi 7, 2007 e 2009 la unit viene compilata come unit vuota e i metodi asincroni non ci sono, quindi GetFuture, ConnectTask e ChatAsync semplicemente non sono disponibili. Tutto il resto della libreria continua a supportare Delphi 7 esattamente come prima, inclusa l'API basata sugli eventi accanto a cui questi metodi si collocano. Non è stato rimosso nulla e nessun codice esistente cambia comportamento, le nuove chiamate sono aggiunte.

I metodi bloccanti Connect, Disconnect e LastError descritti sopra non sono generici, quindi sono disponibili su ogni versione di Delphi supportata.

Esempi funzionanti di tutto questo sono inclusi nel sorgente in sgcBase_AsyncAwait_Examples.pas, e coprono task, future, ritardi, cancellazione, interrogazione dello stato, richieste HTTP parallele e il watchdog di timeout visto sopra.

Disponibilità

Task e future sono disponibili in sgcWebSockets 2026.7 da Delphi 2010 a Delphi 13 e su C++Builder, su Win32/Win64, Linux64, macOS, Android e iOS. I metodi bloccanti Connect, Disconnect e LastError sono disponibili da Delphi 7 a 13.

I clienti con un abbonamento attivo possono scaricare la nuova build dall'area clienti. Gli utenti della versione di prova possono scaricare l'installer aggiornato su esegece.com/products/websockets/download.

Domande, commenti o bisogno di aiuto per convertire una form basata su callback in task? Contattaci, riceverai una risposta dalle persone che hanno scritto il codice.