async / await w Delphi: sgcWebSockets bez procedur obsługi zdarzeń | Blog eSeGeCe

async / await w Delphi: sgcWebSockets bez procedur obsługi zdarzeń

· Komponenty

Do tej pory każde asynchroniczne wywołanie w sgcWebSockets oznaczało procedurę obsługi zdarzenia. Wywoływałeś Get w wątku w tle albo ustawiałeś Active := True i czekałeś na OnConnect. Wynik pojawiał się gdzie indziej, w innej metodzie, więc kod, który rozpoczynał operację, i kod, który konsumował odpowiedź, żyły w różnych miejscach. Dodaj drugie żądanie zależne od pierwszego, do tego obsługę błędów i panel "proszę czekać", który trzeba potem znowu ukryć, a skończysz z małą maszyną stanów rozsianą po całym formularzu: kilka prywatnych pól, żeby pamiętać, co robiłeś, flaga informująca, czy ostatnie wywołanie wciąż jest w toku, i procedura obsługi, która musi ustalić, które z trzech żądań właśnie wróciło.

sgcWebSockets 2026.7 dodaje API w stylu await, które usuwa większość tego balastu. Żądanie HTTP, połączenie WebSocket albo wywołanie czatu AI zwraca teraz obiekt, na który możesz poczekać, do którego możesz podpiąć wywołanie zwrotne albo który możesz anulować, dzięki czemu kod czyta się od góry do dołu, w kolejności, w jakiej się wykonuje. Anulowanie też nie jest tylko kosmetyką, przerywa operację leżącą pod spodem, a nie jedynie ignoruje odpowiedź, gdy ta w końcu nadejdzie.

Zadania i obiekty future

Nowy moduł to sgcBase_AsyncAwait. Wprowadza dwa interfejsy. IsgcTask reprezentuje pracę, która się kończy, ale nie zwraca wartości, a IsgcFuture<T> reprezentuje pracę zwracającą wartość typu T. Oba dają Ci te same cztery rzeczy: Await, aby blokować do zakończenia pracy, ThenProc, aby otrzymać wywołanie zwrotne po sukcesie, OnError, aby otrzymać wywołanie zwrotne przy wyjątku, oraz Cancel, aby przerwać pracę.

Własne zadania możesz zbudować za pomocą TsgcAsync.Run. Przekazana procedura lub funkcja wykonuje się w wątku w tle, a wywołania zwrotne wracają do wątku głównego, więc wewnątrz ThenProc i OnError można bezpiecznie operować na interfejsie użytkownika.

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;

Zadanie utrzymuje się przy życiu, dopóki działa, więc wywołanie typu "uruchom i zapomnij", jak to powyżej, jest w porządku. Jeśli chcesz je później sprawdzić, zachowaj interfejs w zmiennej i odczytuj IsCompleted, IsCancelled lub State, które przyjmuje jedną z wartości tsaPending, tsaRunning, tsaCompleted, tsaFailed lub tsaCancelled. Jest też TsgcAsync.Delay(aMilliseconds), które zwraca zadanie kończące się po upływie czasu i możliwe do anulowania, zanim się wykona, oraz TsgcAsync.RunOnMainThread, które przenosi fragment kodu z powrotem do wątku głównego z wnętrza zadania w tle.

Oczekiwanie na żądanie HTTP

Klient HTTP udostępnia obiekty future bezpośrednio, więc nie musisz sam opakowywać wywołania. GetFuture i PostFuture zwracają IsgcFuture<string>, które rozwiązuje się do treści odpowiedzi.

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;

Jeśli jesteś już w wątku roboczym albo piszesz aplikację konsolową, możesz po prostu zablokować się i odczytać wartość:

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

Await ponownie zgłasza wyjątek, który wystąpił w wywołaniu w tle, opakowany w EsgcAsyncException niosący oryginalną nazwę klasy i komunikat, więc zwykłe try ... except wokół niego nadal działa. Jeśli obiekt future został anulowany, Await zgłasza zamiast tego EsgcTaskCancelled.

Jedno ostrzeżenie ważniejsze od wszystkich innych w tym wpisie. Wywołanie Await w wątku głównym blokuje wątek główny, co w aplikacji VCL lub FMX oznacza zamrożone okno aż do powrotu z wywołania. W formularzu używaj ThenProc. Await zachowaj dla aplikacji konsolowych, usług i kodu, który już działa w wątku w tle. Sam klient HTTP nie jest bezpieczny wątkowo, więc każdemu równoległemu żądaniu daj własną instancję zamiast współdzielić jedną między zadaniami.

Nawiązywanie połączenia klienta WebSocket

Nawiązanie połączenia oznaczało dawniej ustawienie Active := True i oczekiwanie, aż OnConnect powie Ci, że się udało, albo OnError powie, że nie. ConnectTask daje Ci to samo w postaci zadania.

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;

Próba połączenia wykonuje się w wątku w tle, a limit czasu jest prawdziwym limitem czasu. Kiedy upłynie, zadanie kończy się niepowodzeniem, zamiast zawiesić się na stałe. Anulowanie zadania rozłącza gniazdo, co odblokowuje połączenie wciąż czekające na sieć.

Czekanie na połączenie bez await: Connect i LastError

Nie wszystko potrzebuje zadania. Czasem chcesz po prostu linii kodu, która się połączy i powie Ci, czy się udało, czyli tego, czego większość ludzi od początku oczekiwała po Active := True. Wersja 2026.7 dodaje blokujące Connect, które wraca dopiero wtedy, gdy połączenie jest naprawdę nawiązane, a nie tylko rozpoczęte.

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;

To zamyka wyścig, który potrafił zaboleć przy szybkich ponownych połączeniach. Ponieważ Connect zwraca True dopiero wtedy, gdy połączenie jest w pełni gotowe, nic już nie działa na wpół gotowym połączeniu, a Disconnect czeka z powrotem, aż gniazdo naprawdę się zamknie, więc pętla ponownego łączenia nie może nałożyć się na poprzednie połączenie. LastError mieszka na komponencie i przechowuje powód niepowodzenia ostatniej próby, dzięki czemu możesz się dowiedzieć, dlaczego połączenie się nie udało, bez subskrybowania jakiegokolwiek zdarzenia. Jest czyszczone automatycznie na początku każdej próby połączenia, a ClearLastError resetuje je ręcznie.

Pamiętaj, że Connect blokuje wątek wywołujący, dokładnie tak samo jak Await. Używaj go z wątku roboczego lub aplikacji konsolowej, a w formularzu sięgaj po ConnectTask.

Oczekiwanie na czat AI

Klient AI działa według tego samego wzorca. ChatAsync wysyła prompt w wątku w tle i zwraca IsgcFuture<string>, które rozwiązuje się do odpowiedzi modelu, więc wywołanie czatu w formularzu to teraz trzy linie i zero procedur obsługi zdarzeń.

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;

Anulowanie tego obiektu future przerywa żądanie w locie, więc użytkownik, który zamknie okno dialogowe albo naciśnie Stop, nie zostawia w tle działającego wywołania LLM i nie płaci za tokeny, których nigdy nie przeczyta.

Łańcuchowanie zamiast blokowania (ThenProc / OnError)

Warto wyraźnie powiedzieć, który wątek co wykonuje, bo to właśnie tam kod z wywołaniami zwrotnymi zwykle się wykłada. Praca przekazana do TsgcAsync.Run oraz żądanie stojące za GetFuture, ConnectTask czy ChatAsync wykonują się w wątku w tle. Wywołania zwrotne ThenProc i OnError są kolejkowane z powrotem do wątku głównego, więc operowanie w nich na formularzu jest bezpieczne.

Uruchamia się tylko jedno z dwóch. ThenProc wykonuje się, gdy operacja się powiedzie, OnError wykonuje się, gdy zgłosi wyjątek, a żadne z nich nie wykonuje się, jeśli zadanie zostało anulowane. To wystarcza dla przypadku sekwencyjnego: uruchom kolejne wywołanie z wnętrza poprzedniego ThenProc, a łańcuch będzie czytany w kolejności wykonania. Jeśli jesteś już wewnątrz zadania w tle i musisz po drodze dotknąć interfejsu użytkownika, użyj 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);

Anulowanie, które naprawdę anuluje

Większość implementacji "anuluj" ustawia flagę i ignoruje wynik, gdy ten w końcu przyjdzie. Gniazdo pozostaje otwarte, żądanie wciąż działa, a wątek pozostaje zajęty. W sgcWebSockets każde asynchroniczne wywołanie rejestruje w zadaniu akcję anulowania, więc Cancel sięga do operacji pod spodem: anulowanie obiektu future HTTP przerywa żądanie, anulowanie ConnectTask rozłącza gniazdo, a anulowanie ChatAsync przerywa wywołanie AI. Zablokowany wątek roboczy budzi się, zamiast siedzieć bezczynnie do upływu limitu czasu.

Dzięki temu limit czasu łatwo zbudować z zadania Delay i anulowania, co jest wzorcem dostarczanym jako przykład 18 w bibliotece:

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;

Anulowane zadanie nie uruchamia ani ThenProc, ani OnError. Jego State przyjmuje wartość tsaCancelled, IsCancelled zwraca True, a każdy zablokowany w Await dostaje EsgcTaskCancelled. Jeśli budujesz własne zadanie za pomocą TsgcAsync.Run i opakowuje ono coś, co da się przerwać, zarejestruj własne przerwanie przez SetCancelProc, a Cancel je wywoła.

Obsługiwane wersje Delphi

Moduł async/await korzysta z typów generycznych i metod anonimowych, więc wymaga Delphi 2010 lub nowszego. W Delphi 7, 2007 i 2009 moduł kompiluje się do pustego modułu, a metod asynchronicznych po prostu nie ma, więc GetFuture, ConnectTask i ChatAsync nie są dostępne. Cała reszta biblioteki nadal wspiera Delphi 7 dokładnie tak jak wcześniej, łącznie z API opartym na zdarzeniach, obok którego stoją te metody. Nic nie zostało usunięte i żaden istniejący kod nie zmienia zachowania, nowe wywołania są dodatkiem.

Blokujące Connect, Disconnect i LastError opisane powyżej nie są generyczne, więc są dostępne w każdej obsługiwanej wersji Delphi.

Gotowe przykłady tego wszystkiego dostarczane są razem ze źródłami w sgcBase_AsyncAwait_Examples.pas i obejmują zadania, obiekty future, opóźnienia, anulowanie, odpytywanie stanu, równoległe żądania HTTP oraz powyższy watchdog limitu czasu.

Dostępność

Zadania i obiekty future są dostępne w sgcWebSockets 2026.7 od Delphi 2010 do Delphi 13 oraz w C++Builder, na Win32/Win64, Linux64, macOS, Android i iOS. Blokujące Connect, Disconnect i LastError są dostępne w Delphi od 7 do 13.

Klienci z aktywną subskrypcją mogą pobrać nową kompilację ze strefy klienta. Użytkownicy wersji próbnej mogą pobrać zaktualizowany instalator pod adresem esegece.com/products/websockets/download.

Pytania, uwagi albo pomoc w przeniesieniu formularza opartego na wywołaniach zwrotnych na zadania? Skontaktuj się z nami, odpowiedzą Ci osoby, które napisały ten kod.