async / await in Delphi: sgcWebSockets ohne Ereignisbehandlung | eSeGeCe Blog

async / await in Delphi: sgcWebSockets ohne Ereignisbehandlung

· Komponenten

Bis jetzt bedeutete jeder asynchrone Aufruf in sgcWebSockets einen Ereignishandler. Sie riefen Get in einem Hintergrundthread auf, oder Sie setzten Active := True und warteten auf OnConnect. Das Ergebnis kam woanders an, in einer anderen Methode, sodass der Code, der die Operation startete, und der Code, der die Antwort verarbeitete, an verschiedenen Stellen lagen. Fügen Sie eine zweite Anfrage hinzu, die von der ersten abhängt, dazu Fehlerbehandlung und ein Wartehinweis, der auch wieder ausgeblendet werden muss, und Sie enden mit einer kleinen Zustandsmaschine, die über Ihr Formular verstreut ist: ein paar private Felder, um sich zu merken, was Sie gerade taten, ein Flag, um zu wissen, ob der letzte Aufruf noch unterwegs ist, und ein Handler, der herausfinden muss, welche der drei Anfragen gerade zurückgekommen ist.

sgcWebSockets 2026.7 bringt eine await-artige API, die das meiste davon beseitigt. Eine HTTP-Anfrage, ein WebSocket-Verbindungsaufbau oder ein AI-Chat-Aufruf liefert jetzt ein Objekt zurück, auf das Sie warten, an das Sie einen Callback anhängen oder das Sie abbrechen können, sodass sich der Code von oben nach unten in der Reihenfolge liest, in der er abläuft. Der Abbruch ist auch nicht bloß kosmetisch, er bricht die zugrundeliegende Operation wirklich ab, statt die Antwort einfach zu ignorieren, wenn sie schließlich eintrifft.

Tasks und Futures

Die neue Unit heißt sgcBase_AsyncAwait. Sie führt zwei Interfaces ein. IsgcTask steht für Arbeit, die abgeschlossen wird, aber keinen Wert liefert, und IsgcFuture<T> steht für Arbeit, die einen Wert vom Typ T liefert. Beide bieten Ihnen dieselben vier Dinge: Await, um zu blockieren, bis die Arbeit erledigt ist, ThenProc, um bei Erfolg einen Callback zu erhalten, OnError, um bei einer Exception einen Callback zu erhalten, und Cancel, um sie zu stoppen.

Mit TsgcAsync.Run können Sie eigene erzeugen. Die Prozedur oder Funktion, die Sie übergeben, läuft in einem Hintergrundthread, und die Callbacks kommen im Hauptthread zurück, es ist also sicher, innerhalb von ThenProc und OnError die Oberfläche anzufassen.

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;

Der Task hält sich während seiner Laufzeit selbst am Leben, ein Aufruf nach dem Prinzip "abschicken und vergessen" wie oben ist also in Ordnung. Wenn Sie ihn später untersuchen wollen, behalten Sie das Interface in einer Variablen und lesen IsCompleted, IsCancelled oder State, das einen der Werte tsaPending, tsaRunning, tsaCompleted, tsaFailed oder tsaCancelled annimmt. Es gibt außerdem TsgcAsync.Delay(aMilliseconds), das einen Task zurückgibt, der nach einer Zeitspanne abschließt und vor dem Auslösen abgebrochen werden kann, sowie TsgcAsync.RunOnMainThread, das ein Stück Code aus einem Hintergrundtask heraus zurück in den Hauptthread schiebt.

Auf eine HTTP-Anfrage warten

Der HTTP-Client stellt Futures direkt bereit, Sie müssen den Aufruf also nicht selbst umhüllen. GetFuture und PostFuture geben ein IsgcFuture<string> zurück, das sich zum Antwortkörper auflöst.

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;

Wenn Sie sich bereits in einem Worker-Thread befinden oder eine Konsolenanwendung schreiben, können Sie einfach blockieren und den Wert lesen:

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

Await löst die Exception des Hintergrundaufrufs erneut aus, verpackt in einer EsgcAsyncException, die den ursprünglichen Klassennamen und die ursprüngliche Meldung mitführt, ein einfaches try ... except darum herum funktioniert also weiterhin. Wurde das Future abgebrochen, löst Await stattdessen EsgcTaskCancelled aus.

Eine Warnung, die wichtiger ist als alles andere in diesem Beitrag. Ein Aufruf von Await im Hauptthread blockiert den Hauptthread, was in einer VCL- oder FMX-Anwendung ein eingefrorenes Fenster bedeutet, bis der Aufruf zurückkehrt. Verwenden Sie in einem Formular ThenProc. Heben Sie sich Await für Konsolenanwendungen, Dienste und Code auf, der ohnehin schon in einem Hintergrundthread läuft. Der HTTP-Client selbst ist nicht threadsicher, geben Sie also jeder gleichzeitigen Anfrage eine eigene Instanz, statt eine über mehrere Tasks hinweg zu teilen.

Einen WebSocket-Client verbinden

Verbinden bedeutete früher, Active := True zu setzen und darauf zu warten, dass OnConnect den Erfolg meldet, oder OnError das Gegenteil. ConnectTask liefert Ihnen dasselbe als 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;

Der Verbindungsversuch läuft in einem Hintergrundthread, und das Timeout ist ein echtes Timeout. Läuft es ab, schlägt der Task fehl, statt hängen zu bleiben. Der Abbruch des Tasks trennt den Socket, was einen Verbindungsaufbau löst, der noch auf das Netzwerk wartet.

Auf eine Verbindung warten, ohne await: Connect und LastError

Nicht alles braucht einen Task. Manchmal wollen Sie einfach nur eine Codezeile, die verbindet und Ihnen sagt, ob es geklappt hat, also genau das, was die meisten Leute von Active := True von Anfang an erwartet hatten. 2026.7 ergänzt ein blockierendes Connect, das erst zurückkehrt, wenn die Verbindung wirklich aufgebaut ist, und nicht bloß begonnen wurde.

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;

Das schließt eine Race Condition, die früher bei schnellen Wiederverbindungen zubiss. Da Connect erst dann True zurückgibt, wenn die Verbindung vollständig steht, läuft nichts mehr gegen eine halb fertige Verbindung, und Disconnect wartet, bis der Socket wirklich geschlossen ist, bevor es zurückkehrt, sodass sich eine Reconnect-Schleife nicht mehr mit der vorherigen Verbindung überlappen kann. LastError liegt auf der Komponente und enthält den Grund, aus dem der letzte Versuch fehlgeschlagen ist, Sie können also herausfinden, warum ein Verbindungsaufbau gescheitert ist, ohne irgendein Ereignis zu abonnieren. Es wird zu Beginn jedes Verbindungsversuchs automatisch geleert, und ClearLastError setzt es von Hand zurück.

Beachten Sie, dass Connect den aufrufenden Thread blockiert, genau wie Await. Verwenden Sie es aus einem Worker-Thread oder einer Konsolenanwendung heraus, und verwenden Sie ConnectTask in einem Formular.

Auf einen AI-Chat warten

Der AI-Client folgt demselben Muster. ChatAsync sendet den Prompt in einem Hintergrundthread und gibt ein IsgcFuture<string> zurück, das sich zur Antwort des Modells auflöst, ein Chat-Aufruf in einem Formular besteht damit aus drei Zeilen und keinem einzigen Ereignishandler.

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;

Der Abbruch dieses Futures bricht die laufende Anfrage ab, ein Benutzer, der den Dialog schließt oder Stopp drückt, lässt also keinen LLM-Aufruf im Hintergrund weiterlaufen und bezahlt keine Tokens, die er nie lesen wird.

Verketten statt blockieren (ThenProc / OnError)

Es lohnt sich, deutlich zu sagen, welcher Thread was ausführt, denn genau dort geht Callback-Code üblicherweise schief. Die Arbeit, die Sie an TsgcAsync.Run übergeben, und die Anfrage hinter GetFuture, ConnectTask oder ChatAsync laufen alle in einem Hintergrundthread. Die Callbacks ThenProc und OnError werden zurück in den Hauptthread eingereiht, es ist also sicher, darin ein Formular anzufassen.

Nur einer von beiden wird ausgelöst. ThenProc läuft, wenn die Operation erfolgreich ist, OnError läuft, wenn sie eine Exception auslöst, und keiner von beiden läuft, wenn der Task abgebrochen wurde. Für den sequenziellen Fall reicht das aus: Starten Sie den nächsten Aufruf aus dem vorherigen ThenProc heraus, und die Kette liest sich in der Reihenfolge, in der sie ausgeführt wird. Wenn Sie bereits in einem Hintergrundtask sind und zwischendurch die Oberfläche anfassen müssen, verwenden Sie 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);

Ein Abbruch, der wirklich abbricht

Die meisten "Abbrechen"-Implementierungen setzen ein Flag und ignorieren das Ergebnis, wenn es irgendwann eintrifft. Der Socket bleibt offen, die Anfrage läuft weiter, und der Thread bleibt beschäftigt. In sgcWebSockets registriert jeder asynchrone Aufruf eine Abbruchaktion beim Task, sodass Cancel die darunterliegende Operation erreicht: Der Abbruch eines HTTP-Futures bricht die Anfrage ab, der Abbruch eines ConnectTask trennt den Socket, und der Abbruch eines ChatAsync bricht den AI-Aufruf ab. Der blockierte Worker-Thread wacht auf, statt bis zum Ablauf eines Timeouts dazusitzen.

Damit lässt sich ein Timeout leicht aus einem Delay-Task und einem Abbruch bauen, genau das ist das Muster, das als Beispiel 18 mit der Bibliothek ausgeliefert wird:

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;

Ein abgebrochener Task löst weder ThenProc noch OnError aus. Sein State wird zu tsaCancelled, IsCancelled gibt True zurück, und wer in Await blockiert ist, bekommt eine EsgcTaskCancelled. Wenn Sie mit TsgcAsync.Run einen eigenen Task bauen, der etwas Abbrechbares umhüllt, registrieren Sie Ihren eigenen Abbruch mit SetCancelProc, und Cancel ruft ihn auf.

Unterstützte Delphi-Versionen

Die async/await-Unit verwendet Generics und anonyme Methoden, sie benötigt daher Delphi 2010 oder neuer. Unter Delphi 7, 2007 und 2009 kompiliert die Unit zu einer leeren Unit und die asynchronen Methoden existieren nicht, GetFuture, ConnectTask und ChatAsync stehen dort also schlicht nicht zur Verfügung. Alles andere in der Bibliothek unterstützt Delphi 7 weiterhin genau wie zuvor, einschließlich der ereignisbasierten API, neben der diese Methoden stehen. Es wurde nichts entfernt und kein bestehender Code ändert sein Verhalten, die neuen Aufrufe sind reine Ergänzungen.

Das blockierende Connect, Disconnect und LastError, die oben beschrieben sind, verwenden keine Generics, sie stehen daher in jeder unterstützten Delphi-Version zur Verfügung.

Ausgearbeitete Beispiele für all das werden mit dem Quellcode in sgcBase_AsyncAwait_Examples.pas ausgeliefert und decken Tasks, Futures, Verzögerungen, Abbruch, das Abfragen des Zustands, parallele HTTP-Anfragen und den Timeout-Watchdog von oben ab.

Verfügbarkeit

Tasks und Futures stehen in sgcWebSockets 2026.7 von Delphi 2010 bis Delphi 13 und auf C++Builder zur Verfügung, unter Win32/Win64, Linux64, macOS, Android und iOS. Das blockierende Connect, Disconnect und LastError stehen unter Delphi 7 bis 13 zur Verfügung.

Kunden mit einem aktiven Abonnement können den neuen Build im Kundenbereich herunterladen. Trial-Nutzer erhalten das aktualisierte Installationsprogramm unter esegece.com/products/websockets/download.

Fragen, Feedback oder Hilfe dabei, ein Callback-basiertes Formular auf Tasks umzustellen? Nehmen Sie Kontakt auf, Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.