Delphi の async / await: イベントハンドラー不要の sgcWebSockets | eSeGeCe ブログ

Delphi の async / await: イベントハンドラー不要の sgcWebSockets

· コンポーネント

これまで、sgcWebSockets における非同期呼び出しは必ずイベントハンドラーを意味していました。バックグラウンドスレッドで Get を呼ぶか、Active := True を設定して OnConnect を待つ。結果は別の場所、別のメソッドに届くため、処理を開始するコードと答えを受け取るコードは別々の場所に存在していました。ここに、最初のリクエストに依存する2つ目のリクエスト、エラー処理、そして後で非表示に戻さなければならない「お待ちください」パネルを加えると、フォーム全体に散らばった小さなステートマシンができあがります。何をしていたかを覚えておくためのプライベートフィールドがいくつか、直前の呼び出しがまだ実行中かどうかを知るためのフラグ、そして3つのリクエストのどれが返ってきたのかを判別しなければならないハンドラー。

sgcWebSockets 2026.7 は、その多くを解消する await スタイルの API を追加します。HTTP リクエスト、WebSocket の接続、AI のチャット呼び出しは、待機したり、コールバックを連結したり、キャンセルしたりできるオブジェクトを返すようになったので、コードは実際に起こる順番どおりに上から下へと読めます。キャンセルも見せかけではありません。答えが最終的に届いたときに無視するのではなく、下層の処理そのものを中断します。

タスクとフューチャー

新しいユニットは sgcBase_AsyncAwait です。ここでは2つのインターフェイスを導入しています。IsgcTask は完了はするが値を生成しない処理を表し、IsgcFuture<T> は型 T の値を生成する処理を表します。どちらも同じ4つの機能を提供します。処理が完了するまでブロックする Await、成功時にコールバックを受け取る ThenProc、例外発生時にコールバックを受け取る OnError、そして処理を停止する Cancel です。

TsgcAsync.Run で独自のものを組み立てられます。渡したプロシージャーまたは関数はバックグラウンドスレッドで実行され、コールバックはメインスレッドに戻ってくるので、ThenProcOnError の中で UI に触れても安全です。

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;

タスクは実行中、自分自身を生存させ続けるので、上のような撃ちっぱなしの呼び出しでも問題ありません。あとから状態を調べたい場合は、インターフェイスを変数に保持して IsCompletedIsCancelled、あるいは State を読み取ってください。これは tsaPendingtsaRunningtsaCompletedtsaFailedtsaCancelled のいずれかです。さらに、タイムアウト後に完了し、発火前にキャンセルできるタスクを返す TsgcAsync.Delay(aMilliseconds) と、バックグラウンドタスクの内側からメインスレッドへコードを戻して実行する TsgcAsync.RunOnMainThread もあります。

HTTP リクエストを await する

HTTP クライアントはフューチャーを直接公開しているので、自分で呼び出しをラップする必要はありません。GetFuturePostFuture は、レスポンスボディに解決される IsgcFuture<string> を返します。

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;

すでにワーカースレッド上にいる場合や、コンソールアプリケーションを書いている場合は、単純にブロックして値を読み取れます。

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

Await は、バックグラウンドの呼び出しが送出した例外を、元のクラス名とメッセージを保持する EsgcAsyncException でラップして再送出するので、その周囲の素朴な try ... except がそのまま機能します。フューチャーがキャンセルされていた場合、Await は代わりに EsgcTaskCancelled を送出します。

この記事の中で何よりも重要な警告がひとつあります。メインスレッドで Await を呼ぶとメインスレッドがブロックされます。VCL や FMX のアプリケーションでは、呼び出しが戻るまでウィンドウが固まることを意味します。フォームでは ThenProc を使ってください。Await はコンソールアプリケーション、サービス、そしてすでにバックグラウンドスレッド上で動いているコードのために取っておきましょう。HTTP クライアント自体はスレッドセーフではないので、1つのインスタンスを複数のタスクで共有するのではなく、同時実行するリクエストごとに専用のインスタンスを与えてください。

WebSocket クライアントの接続

従来、接続するとは Active := True を設定し、成功を伝える OnConnect、または失敗を伝える OnError を待つことでした。ConnectTask は同じことをタスクとして提供します。

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;

接続の試行はバックグラウンドスレッドで実行され、タイムアウトは本物のタイムアウトです。期限が切れると、タスクはハングするのではなく失敗します。タスクをキャンセルするとソケットが切断されるので、ネットワーク待ちで止まっている接続処理を解放できます。

await せずに接続を待つ: Connect と LastError

すべてにタスクが必要なわけではありません。ときには、接続して成功したかどうかを教えてくれる一行のコードが欲しいだけのこともあります。それこそ、多くの人が最初から Active := True に期待していたことでしょう。2026.7 は、接続が単に開始されたときではなく本当に確立されたときにだけ戻る、ブロッキングの Connect を追加します。

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;

これは、素早い再接続の際に悩まされていた競合状態を解消します。Connect は接続が完全に確立されてはじめて True を返すので、半端な状態の接続に対して何かが実行されることはもうありません。また Disconnect はソケットが本当に閉じられるまで待ってから戻るので、再接続ループが前の接続と重なることはありません。LastError はコンポーネント上に存在し、直前の試行が失敗した理由を保持するので、どのイベントも購読することなく接続失敗の理由を知ることができます。これは各接続試行の開始時に自動的にクリアされ、ClearLastError で手動でもリセットできます。

ConnectAwait とまったく同じく呼び出し元スレッドをブロックする点に注意してください。ワーカースレッドやコンソールアプリケーションから使い、フォームでは ConnectTask を使ってください。

AI チャットを await する

AI クライアントも同じパターンに従います。ChatAsync はプロンプトをバックグラウンドスレッドで送信し、モデルの応答に解決される IsgcFuture<string> を返すので、フォームでのチャット呼び出しは3行になり、イベントハンドラーは不要です。

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;

このフューチャーをキャンセルすると、実行中のリクエストが中断されます。ダイアログを閉じたり停止ボタンを押したりしたユーザーが、読むことのないトークンに課金されながら LLM の呼び出しをバックグラウンドで走らせ続けることはありません。

ブロックするのではなく連結する (ThenProc / OnError)

どのスレッドが何を実行するのかを明示しておく価値があります。コールバックのコードがうまくいかなくなるのは、たいていそこだからです。TsgcAsync.Run に渡した処理も、GetFutureConnectTaskChatAsync の背後にあるリクエストも、すべてバックグラウンドスレッドで実行されます。ThenProcOnError のコールバックはメインスレッドへキューイングされて戻るので、その中でフォームに触れても安全です。

この2つのうち発火するのは一方だけです。ThenProc は処理が成功したときに実行され、OnError は例外が送出されたときに実行され、タスクがキャンセルされた場合はどちらも実行されません。逐次的なケースにはこれで十分です。直前の ThenProc の中から次の呼び出しを開始すれば、チェーンは実行される順番どおりに読めます。すでにバックグラウンドタスクの中にいて、途中で UI に触れる必要がある場合は、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);

本当にキャンセルするキャンセル

たいていの「キャンセル」の実装は、フラグを立てて、結果が最終的に届いたときにそれを無視するだけです。ソケットは開いたまま、リクエストは走り続け、スレッドは塞がったままです。sgcWebSockets では、各非同期呼び出しがタスクにキャンセル動作を登録するので、Cancel は下層の処理まで届きます。HTTP のフューチャーをキャンセルするとリクエストが中断され、ConnectTask をキャンセルするとソケットが切断され、ChatAsync をキャンセルすると AI の呼び出しが中断されます。ブロックされていたワーカースレッドは、タイムアウトが切れるまで座り込むのではなく、その場で目を覚まします。

これにより、Delay タスクとキャンセルを組み合わせてタイムアウトを簡単に構築できます。ライブラリでは例18として同梱されているパターンです。

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;

キャンセルされたタスクは ThenProcOnError も発火しません。その StatetsaCancelled になり、IsCancelledTrue を返し、Await でブロックしていた側は EsgcTaskCancelled を受け取ります。TsgcAsync.Run で独自のタスクを組み立て、それが中断可能な何かをラップしているなら、SetCancelProc で自前の中断処理を登録すれば Cancel がそれを呼び出します。

対応する Delphi のバージョン

async/await のユニットはジェネリクスと匿名メソッドを使うため、Delphi 2010 以降が必要です。Delphi 7、2007、2009 ではこのユニットは空のユニットとしてコンパイルされ、非同期メソッドは存在しないので、GetFutureConnectTaskChatAsync は単純に利用できません。ライブラリのそれ以外の部分は、これらのメソッドと並んで存在するイベントベースの API を含め、これまでどおり Delphi 7 をサポートしています。何も削除されておらず、既存のコードの挙動が変わることもありません。新しい呼び出しは追加されたものです。

上で説明したブロッキングの ConnectDisconnectLastError はジェネリックではないので、サポートされているすべての Delphi バージョンで利用できます。

これらすべての実例は、ソースに含まれる sgcBase_AsyncAwait_Examples.pas に同梱されており、タスク、フューチャー、遅延、キャンセル、状態のポーリング、並列 HTTP リクエスト、そして上記のタイムアウト監視をカバーしています。

提供状況

タスクとフューチャーは、sgcWebSockets 2026.7 において Delphi 2010 から Delphi 13 まで、および C++Builder で、Win32/Win64、Linux64、macOS、Android、iOS にわたって利用できます。ブロッキングの ConnectDisconnectLastError は Delphi 7 から 13 で利用できます。

有効なサブスクリプションをお持ちのお客様は、カスタマーエリアから新しいビルドをダウンロードできます。トライアルのユーザーは esegece.com/products/websockets/download から更新されたインストーラーを入手できます。

ご質問やご意見、あるいはコールバックベースのフォームをタスクへ移行する際のお手伝いが必要ですか。お問い合わせください。コードを書いた本人が返信します。