async / await in Delphi: sgcWebSockets zonder event handlers | eSeGeCe Blog

async / await in Delphi: sgcWebSockets zonder event handlers

· Componenten

Tot nu toe betekende elke asynchrone aanroep in sgcWebSockets een event handler. U riep Get aan op een achtergrondthread, of u zette Active := True en wachtte op OnConnect. Het resultaat kwam ergens anders aan, in een andere methode, dus de code die de operatie startte en de code die het antwoord verwerkte stonden op verschillende plekken. Voeg daar een tweede verzoek aan toe dat van het eerste afhangt, plus foutafhandeling, plus een "even geduld"-paneel dat weer verborgen moet worden, en u eindigt met een kleine toestandsmachine die over uw formulier is uitgesmeerd: een paar private velden om te onthouden waar u mee bezig was, een vlag om te weten of de laatste aanroep nog loopt, en een handler die moet uitzoeken welk van de drie verzoeken zojuist terugkwam.

sgcWebSockets 2026.7 voegt een await-achtige API toe die het meeste daarvan wegneemt. Een HTTP-verzoek, een WebSocket-verbinding of een AI-chataanroep geeft nu een object terug waarop u kunt wachten, waaraan u een callback kunt koppelen, of dat u kunt annuleren, zodat de code van boven naar beneden leest in de volgorde waarin hij gebeurt. Annuleren is bovendien niet cosmetisch, het breekt de onderliggende operatie af in plaats van het antwoord simpelweg te negeren wanneer het uiteindelijk arriveert.

Tasks en futures

De nieuwe unit is sgcBase_AsyncAwait. Die introduceert twee interfaces. IsgcTask vertegenwoordigt werk dat afrondt maar geen waarde oplevert, en IsgcFuture<T> vertegenwoordigt werk dat een waarde van het type T oplevert. Beide geven u dezelfde vier dingen: Await om te blokkeren tot het werk klaar is, ThenProc voor een callback wanneer het slaagt, OnError voor een callback wanneer het een exception opwerpt, en Cancel om het te stoppen.

U kunt uw eigen taak bouwen met TsgcAsync.Run. De procedure of functie die u meegeeft draait op een achtergrondthread, en de callbacks komen terug op de hoofdthread, dus het is veilig om de UI aan te raken binnen ThenProc en 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;

De task houdt zichzelf in leven zolang hij draait, dus een fire-and-forget aanroep zoals hierboven is prima. Wilt u hem later inspecteren, bewaar de interface dan in een variabele en lees IsCompleted, IsCancelled of State, die een van tsaPending, tsaRunning, tsaCompleted, tsaFailed of tsaCancelled is. Er is ook TsgcAsync.Delay(aMilliseconds), die een task teruggeeft die na een timeout afrondt en geannuleerd kan worden voordat hij afgaat, en TsgcAsync.RunOnMainThread, die vanuit een achtergrondtaak een stuk code terugduwt naar de hoofdthread.

Wachten op een HTTP-verzoek

De HTTP-client stelt futures rechtstreeks beschikbaar, dus u hoeft de aanroep niet zelf te verpakken. GetFuture en PostFuture geven een IsgcFuture<string> terug die oplost naar de responsbody.

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;

Zit u al op een worker-thread, of schrijft u een consoletoepassing, dan kunt u eenvoudigweg blokkeren en de waarde lezen:

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

Await werpt opnieuw op wat de achtergrondaanroep opwierp, verpakt in een EsgcAsyncException die de oorspronkelijke klassenaam en het bericht meedraagt, dus een gewone try ... except eromheen werkt nog steeds. Als de future geannuleerd was, werpt Await in plaats daarvan EsgcTaskCancelled op.

Eén waarschuwing die belangrijker is dan alle andere in dit artikel. Await aanroepen op de hoofdthread blokkeert de hoofdthread, wat in een VCL- of FMX-toepassing een bevroren venster betekent tot de aanroep terugkeert. Gebruik in een formulier ThenProc. Bewaar Await voor consoletoepassingen, services, en code die al op een achtergrondthread draait. De HTTP-client zelf is niet thread-safe, geef dus elk gelijktijdig verzoek zijn eigen instantie in plaats van er één te delen tussen taken.

Een WebSocket-client verbinden

Verbinden betekende vroeger Active := True zetten en wachten tot OnConnect u vertelde dat het gelukt was, of tot OnError u vertelde dat het niet gelukt was. ConnectTask geeft u hetzelfde als een 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;

De verbindingspoging draait op een achtergrondthread en de timeout is een echte timeout. Als die verstrijkt, faalt de task in plaats van te blijven hangen. Het annuleren van de task verbreekt de socket, wat een connect deblokkeert die nog op het netwerk staat te wachten.

Wachten op een verbinding zonder await: Connect en LastError

Niet alles heeft een task nodig. Soms wilt u gewoon een regel code die verbindt en u vertelt of het gelukt is, precies wat de meeste mensen sowieso al van Active := True verwachtten. 2026.7 voegt een blokkerende Connect toe die pas terugkeert wanneer de verbinding echt tot stand is gebracht, niet louter gestart.

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;

Dit sluit een race die vroeger toesloeg bij snelle reconnects. Omdat Connect pas True teruggeeft zodra de verbinding volledig staat, draait er niets meer tegen een half gereedde verbinding, en Disconnect wacht tot de socket echt gesloten is voordat hij terugkeert, zodat een reconnect-lus de vorige verbinding niet kan overlappen. LastError zit op de component en bevat de reden waarom de laatste poging mislukte, zodat u kunt achterhalen waarom een connect mislukte zonder u op een event te abonneren. Hij wordt automatisch gewist aan het begin van elke verbindingspoging, en ClearLastError zet hem met de hand terug.

Let op: Connect blokkeert de aanroepende thread, precies zoals Await dat doet. Gebruik hem vanuit een worker-thread of een consoletoepassing, en gebruik ConnectTask vanuit een formulier.

Wachten op een AI-chat

De AI-client volgt hetzelfde patroon. ChatAsync stuurt de prompt op een achtergrondthread en geeft een IsgcFuture<string> terug die oplost naar het antwoord van het model, dus een chataanroep in een formulier is nu drie regels en geen enkele event handler.

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;

Het annuleren van die future breekt het lopende verzoek af, zodat een gebruiker die het dialoogvenster sluit of op Stop drukt geen LLM-aanroep op de achtergrond laat doorlopen en niet betaalt voor tokens die hij nooit zal lezen.

Koppelen in plaats van blokkeren (ThenProc / OnError)

Het is de moeite waard om expliciet te zijn over welke thread wat draait, want daar gaat callback-code meestal mis. Het werk dat u aan TsgcAsync.Run meegeeft, en het verzoek achter GetFuture, ConnectTask of ChatAsync, draaien allemaal op een achtergrondthread. De ThenProc- en OnError-callbacks worden teruggezet in de wachtrij van de hoofdthread, dus een formulier aanraken binnen die callbacks is veilig.

Slechts een van de twee gaat af. ThenProc draait wanneer de operatie slaagt, OnError draait wanneer er een exception optreedt, en geen van beide draait als de task geannuleerd is. Dat volstaat voor het sequentiële geval: start de volgende aanroep vanuit de vorige ThenProc en de keten leest in de volgorde waarin hij wordt uitgevoerd. Zit u al in een achtergrondtaak en moet u halverwege de UI aanraken, gebruik dan 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);

Annuleren dat echt annuleert

De meeste "cancel"-implementaties zetten een vlag en negeren het resultaat wanneer het uiteindelijk arriveert. De socket blijft open, het verzoek blijft draaien, en de thread blijft bezet. In sgcWebSockets registreert elke asynchrone aanroep een cancel-actie bij de task, zodat Cancel de onderliggende operatie bereikt: het annuleren van een HTTP-future breekt het verzoek af, het annuleren van een ConnectTask verbreekt de socket, en het annuleren van een ChatAsync breekt de AI-aanroep af. De geblokkeerde worker-thread wordt wakker in plaats van te blijven zitten tot een timeout verstrijkt.

Daardoor is een timeout eenvoudig te bouwen uit een Delay-task en een cancel, het patroon dat als voorbeeld 18 met de bibliotheek wordt meegeleverd:

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;

Een geannuleerde task vuurt noch ThenProc noch OnError af. Zijn State wordt tsaCancelled, IsCancelled geeft True terug, en wie geblokkeerd is in Await krijgt een EsgcTaskCancelled. Bouwt u uw eigen task met TsgcAsync.Run en verpakt die iets wat afgebroken kan worden, registreer dan uw eigen abort met SetCancelProc, en Cancel zal die aanroepen.

Ondersteunde Delphi-versies

De async/await-unit gebruikt generics en anonieme methodes, dus die heeft Delphi 2010 of nieuwer nodig. Op Delphi 7, 2007 en 2009 compileert de unit tot een lege unit en zijn de async-methodes er niet, dus GetFuture, ConnectTask en ChatAsync zijn simpelweg niet beschikbaar. Al het andere in de bibliotheek ondersteunt Delphi 7 nog precies zoals voorheen, inclusief de event-gebaseerde API waar deze methodes naast staan. Er is niets verwijderd en geen enkele bestaande code verandert van gedrag, de nieuwe aanroepen zijn toevoegingen.

De blokkerende Connect, Disconnect en LastError die hierboven zijn beschreven zijn niet generiek, dus ze zijn beschikbaar op elke ondersteunde Delphi-versie.

Uitgewerkte voorbeelden van dit alles worden met de broncode meegeleverd in sgcBase_AsyncAwait_Examples.pas, met tasks, futures, delays, annulering, het pollen van de state, parallelle HTTP-verzoeken en de timeout-watchdog van hierboven.

Beschikbaarheid

Tasks en futures zijn beschikbaar in sgcWebSockets 2026.7 vanaf Delphi 2010 tot en met Delphi 13 en op C++Builder, op Win32/Win64, Linux64, macOS, Android en iOS. De blokkerende Connect, Disconnect en LastError zijn beschikbaar op Delphi 7 tot en met 13.

Klanten met een actief abonnement kunnen de nieuwe build downloaden in het klantengedeelte. Trialgebruikers kunnen het bijgewerkte installatieprogramma ophalen op esegece.com/products/websockets/download.

Vragen, feedback of hulp bij het omzetten van een callback-gebaseerd formulier naar tasks? Neem contact op, u krijgt antwoord van de mensen die de code hebben geschreven.