async / await no Delphi: sgcWebSockets sem manipuladores de eventos | Blog eSeGeCe

async / await no Delphi: sgcWebSockets sem manipuladores de eventos

· Componentes

Até agora, toda chamada assíncrona no sgcWebSockets significava um manipulador de eventos. Você chamava Get em uma thread em segundo plano, ou definia Active := True e esperava por OnConnect. O resultado chegava em outro lugar, em outro método, então o código que iniciava a operação e o código que consumia a resposta viviam em lugares diferentes. Acrescente uma segunda requisição que depende da primeira, mais o tratamento de erros, mais um painel de "aguarde" que precisa ser escondido de novo, e você acaba com uma pequena máquina de estados espalhada pelo seu formulário: alguns campos privados para lembrar o que você estava fazendo, um sinalizador para saber se a última chamada ainda está em andamento, e um manipulador que precisa descobrir qual das três requisições acabou de voltar.

O sgcWebSockets 2026.7 adiciona uma API no estilo await que elimina a maior parte disso. Uma requisição HTTP, uma conexão WebSocket ou uma chamada de chat de IA agora retornam um objeto que você pode aguardar, encadear a um callback ou cancelar, de modo que o código é lido de cima para baixo na ordem em que acontece. O cancelamento também não é cosmético, ele aborta a operação subjacente em vez de simplesmente ignorar a resposta quando ela finalmente chega.

Tarefas e futuros

A nova unidade é sgcBase_AsyncAwait. Ela introduz duas interfaces. IsgcTask representa um trabalho que termina mas não produz um valor, e IsgcFuture<T> representa um trabalho que produz um valor do tipo T. Ambas oferecem as mesmas quatro coisas: Await para bloquear até o trabalho terminar, ThenProc para receber um callback quando ele tem sucesso, OnError para receber um callback quando ele levanta uma exceção, e Cancel para pará-lo.

Você pode construir os seus próprios com TsgcAsync.Run. O procedimento ou função que você passa é executado em uma thread em segundo plano, e os callbacks voltam na thread principal, então é seguro tocar na interface gráfica dentro de 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;

A tarefa mantém a si mesma viva enquanto é executada, então uma chamada do tipo dispare e esqueça, como a de cima, está correta. Se você quiser inspecioná-la mais tarde, guarde a interface em uma variável e leia IsCompleted, IsCancelled ou State, que é um entre tsaPending, tsaRunning, tsaCompleted, tsaFailed ou tsaCancelled. Há também TsgcAsync.Delay(aMilliseconds), que retorna uma tarefa que se completa após um tempo limite e pode ser cancelada antes de disparar, e TsgcAsync.RunOnMainThread, que empurra um trecho de código de volta para a thread principal a partir de dentro de uma tarefa em segundo plano.

Aguardando uma requisição HTTP

O cliente HTTP expõe futuros diretamente, então você não precisa envolver a chamada por conta própria. GetFuture e PostFuture retornam um IsgcFuture<string> que resolve para o corpo da resposta.

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 você já está em uma thread de trabalho, ou está escrevendo uma aplicação de console, pode simplesmente bloquear e ler o valor:

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

Await relança o que quer que a chamada em segundo plano tenha levantado, embrulhado em uma EsgcAsyncException que carrega o nome da classe original e a mensagem, então um simples try ... except em volta dela continua funcionando. Se o futuro foi cancelado, Await levanta EsgcTaskCancelled em vez disso.

Um aviso que importa mais do que qualquer outro neste artigo. Chamar Await na thread principal bloqueia a thread principal, o que em uma aplicação VCL ou FMX significa uma janela congelada até que a chamada retorne. Em um formulário, use ThenProc. Reserve Await para aplicações de console, serviços e código que já está sendo executado em uma thread em segundo plano. O próprio cliente HTTP não é seguro para uso concorrente, então dê a cada requisição simultânea a sua própria instância em vez de compartilhar uma entre tarefas.

Conectando um cliente WebSocket

Conectar costumava significar definir Active := True e esperar que OnConnect dissesse que funcionou, ou que OnError dissesse que não. ConnectTask dá a você a mesma coisa como uma tarefa.

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;

A tentativa de conexão roda em uma thread em segundo plano e o tempo limite é um tempo limite de verdade. Se ele expirar, a tarefa falha em vez de ficar pendurada. Cancelar a tarefa desconecta o socket, o que desbloqueia uma conexão que ainda está esperando na rede.

Esperar por uma conexão sem aguardar: Connect e LastError

Nem tudo precisa de uma tarefa. Às vezes você só quer uma linha de código que conecte e diga se funcionou, que é o que a maioria das pessoas esperava que Active := True fizesse desde o começo. A 2026.7 adiciona um Connect bloqueante que retorna somente quando a conexão está realmente estabelecida, não apenas iniciada.

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;

Isso fecha uma condição de corrida que costumava incomodar durante reconexões rápidas. Como Connect só retorna True depois que a conexão está totalmente no ar, nada mais é executado contra uma conexão pela metade, e Disconnect espera até que o socket esteja realmente fechado antes de retornar, de modo que um laço de reconexão não pode se sobrepor à conexão anterior. LastError vive no componente e guarda o motivo pelo qual a última tentativa falhou, então você pode descobrir por que uma conexão falhou sem assinar nenhum evento. Ele é limpo automaticamente no início de cada tentativa de conexão, e ClearLastError o reinicia manualmente.

Observe que Connect bloqueia a thread chamadora, exatamente como Await faz. Use-o a partir de uma thread de trabalho ou de uma aplicação de console, e use ConnectTask a partir de um formulário.

Aguardando um chat de IA

O cliente de IA segue o mesmo padrão. ChatAsync envia o prompt em uma thread em segundo plano e retorna um IsgcFuture<string> que resolve para a resposta do modelo, então uma chamada de chat em um formulário agora são três linhas e nenhum manipulador de eventos.

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;

Cancelar esse futuro aborta a requisição em andamento, de modo que um usuário que fecha a caixa de diálogo ou pressiona Parar não deixa uma chamada de LLM rodando em segundo plano e pagando por tokens que nunca vai ler.

Encadear em vez de bloquear (ThenProc / OnError)

Vale a pena ser explícito sobre qual thread executa o quê, porque é aí que o código de callback costuma dar errado. O trabalho que você passa para TsgcAsync.Run, e a requisição por trás de GetFuture, ConnectTask ou ChatAsync, todos rodam em uma thread em segundo plano. Os callbacks ThenProc e OnError são enfileirados de volta para a thread principal, então tocar em um formulário dentro deles é seguro.

Apenas um dos dois dispara. ThenProc é executado quando a operação tem sucesso, OnError é executado quando ela levanta uma exceção, e nenhum dos dois é executado se a tarefa foi cancelada. Isso é suficiente para o caso sequencial: inicie a próxima chamada de dentro do ThenProc anterior e a cadeia é lida na ordem em que executa. Se você já está dentro de uma tarefa em segundo plano e precisa tocar na interface gráfica no meio do caminho, use 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);

Um cancelamento que realmente cancela

A maioria das implementações de "cancelar" levanta um sinalizador e ignora o resultado quando ele finalmente chega. O socket continua aberto, a requisição continua rodando e a thread continua ocupada. No sgcWebSockets cada chamada assíncrona registra uma ação de cancelamento na tarefa, então Cancel alcança a operação que está por baixo: cancelar um futuro HTTP aborta a requisição, cancelar um ConnectTask desconecta o socket, e cancelar um ChatAsync aborta a chamada de IA. A thread de trabalho bloqueada acorda em vez de ficar ali parada até que um tempo limite expire.

Isso torna fácil construir um tempo limite a partir de uma tarefa Delay e de um cancelamento, que é o padrão distribuído como o exemplo 18 da biblioteca:

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;

Uma tarefa cancelada não dispara nem ThenProc nem OnError. Seu State passa a ser tsaCancelled, IsCancelled retorna True, e quem estiver bloqueado em Await recebe uma EsgcTaskCancelled. Se você construir a sua própria tarefa com TsgcAsync.Run e ela envolver algo que pode ser abortado, registre o seu próprio aborto com SetCancelProc e Cancel vai chamá-lo.

Suporte a versões do Delphi

A unidade de async/await usa generics e métodos anônimos, então precisa do Delphi 2010 ou mais recente. No Delphi 7, 2007 e 2009 a unidade compila como uma unidade vazia e os métodos assíncronos não existem, então GetFuture, ConnectTask e ChatAsync simplesmente não estão disponíveis. Todo o resto da biblioteca continua a suportar o Delphi 7 exatamente como antes, incluindo a API baseada em eventos ao lado da qual esses métodos ficam. Nada foi removido e nenhum código existente muda de comportamento, as novas chamadas são adições.

Os métodos bloqueantes Connect, Disconnect e LastError descritos acima não usam generics, então estão disponíveis em todas as versões suportadas do Delphi.

Exemplos completos de tudo isso acompanham o código-fonte em sgcBase_AsyncAwait_Examples.pas, cobrindo tarefas, futuros, atrasos, cancelamento, consulta do estado, requisições HTTP em paralelo e o watchdog de tempo limite acima.

Disponibilidade

Tarefas e futuros estão disponíveis no sgcWebSockets 2026.7 do Delphi 2010 até o Delphi 13 e no C++Builder, em Win32/Win64, Linux64, macOS, Android e iOS. Os métodos bloqueantes Connect, Disconnect e LastError estão disponíveis do Delphi 7 ao 13.

Clientes com assinatura ativa podem baixar a nova versão na área de clientes. Usuários de avaliação podem obter o instalador atualizado em esegece.com/products/websockets/download.

Dúvidas, comentários ou ajuda para portar um formulário baseado em callbacks para tarefas? Entre em contato, você receberá uma resposta das pessoas que escreveram o código.