直到现在,sgcWebSockets 中的每一次异步调用都意味着一个事件处理器。你在后台线程上调用 Get,或者设置 Active := True 然后等待 OnConnect。结果出现在别的地方,出现在另一个方法里,于是发起操作的代码和消费结果的代码分处两地。再加上一个依赖前一个请求的第二个请求,加上错误处理,再加上一个必须被重新隐藏的"请稍候"面板,你最终会得到一个散落在整个窗体上的小型状态机:几个私有字段用来记住你在做什么,一个标志用来判断上一次调用是否还在进行中,以及一个必须搞清楚三个请求中是哪一个刚刚返回的处理器。
sgcWebSockets 2026.7 新增了一套 await 风格的 API,把这些大部分都去掉了。一个 HTTP 请求、一次 WebSocket 连接或一次 AI 聊天调用,现在会返回一个你可以等待、可以链式挂回调、也可以取消的对象,于是代码从上到下按发生的顺序阅读。取消也不只是装样子,它会中止底层的操作,而不是在答案最终到来时把它忽略掉。
任务与 future
新的单元是 sgcBase_AsyncAwait。它引入了两个接口。IsgcTask 表示会完成但不产生返回值的工作,IsgcFuture<T> 表示产生一个 T 类型返回值的工作。两者都提供同样的四样东西:Await 阻塞直到工作完成,ThenProc 在成功时得到回调,OnError 在抛出异常时得到回调,Cancel 用来停止它。
你可以用 TsgcAsync.Run 构建自己的任务。你传入的过程或函数会在后台线程上运行,而回调会回到主线程,所以在 ThenProc 和 OnError 里操作 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;
任务在运行期间会让自己保持存活,所以像上面这样发射后不管的调用是没问题的。如果你想稍后检查它,把接口保存在一个变量里,然后读取 IsCompleted、IsCancelled 或 State,后者是 tsaPending、tsaRunning、tsaCompleted、tsaFailed 或 tsaCancelled 之一。还有 TsgcAsync.Delay(aMilliseconds),它返回一个在超时后完成的任务,并且可以在触发前被取消;以及 TsgcAsync.RunOnMainThread,它可以从后台任务内部把一段代码推回主线程执行。
await 一个 HTTP 请求
HTTP 客户端直接暴露了 future,所以你不必自己去包装调用。GetFuture 和 PostFuture 返回一个解析为响应正文的 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 仍然有效。如果 future 被取消了,Await 抛出的则是 EsgcTaskCancelled。
有一条警告比这篇文章里其他任何内容都更重要。在主线程上调用 Await 会阻塞主线程,在 VCL 或 FMX 应用中这意味着窗口会一直冻结,直到调用返回。在窗体中,请使用 ThenProc。把 Await 留给控制台应用、服务,以及已经运行在后台线程上的代码。HTTP 客户端本身不是线程安全的,所以请为每个并发请求分配各自的实例,而不要在多个任务之间共用一个。
连接一个 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 则可以手动重置它。
请注意 Connect 会阻塞调用线程,就像 Await 一样。请在工作线程或控制台应用中使用它,在窗体中则使用 ConnectTask。
await 一次 AI 聊天
AI 客户端遵循同样的模式。ChatAsync 在后台线程上发送提示词,并返回一个解析为模型回复的 IsgcFuture<string>,所以窗体中的一次聊天调用现在只需三行代码,不需要任何事件处理器。
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;
取消那个 future 会中止正在进行中的请求,所以关闭对话框或按下停止的用户,不会让一次 LLM 调用继续在后台跑,也不会为他们永远不会读的 token 付费。
用链式调用代替阻塞(ThenProc / OnError)
明确说清楚哪个线程运行什么是值得的,因为回调代码通常就是在这里出错的。你传给 TsgcAsync.Run 的工作,以及 GetFuture、ConnectTask 或 ChatAsync 背后的请求,全都运行在后台线程上。ThenProc 和 OnError 回调会被排队回主线程,所以在它们内部操作窗体是安全的。
两者只会触发其中一个。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 future 会中止请求,取消一个 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;
被取消的任务既不会触发 ThenProc,也不会触发 OnError。它的 State 变为 tsaCancelled,IsCancelled 返回 True,而任何阻塞在 Await 上的代码都会得到一个 EsgcTaskCancelled。如果你用 TsgcAsync.Run 构建自己的任务,而它包装的是某个可中止的操作,那就用 SetCancelProc 注册你自己的中止逻辑,Cancel 就会调用它。
Delphi 版本支持
async/await 单元使用了泛型和匿名方法,所以它需要 Delphi 2010 或更新版本。在 Delphi 7、2007 和 2009 上,该单元会编译成一个空单元,异步方法不存在,因此 GetFuture、ConnectTask 和 ChatAsync 根本不可用。库中其他所有内容仍然完全像以前一样支持 Delphi 7,包括这些方法旁边基于事件的 API。没有任何东西被移除,也没有任何现有代码的行为发生改变,这些新调用都是新增的。
上面描述的阻塞式 Connect、Disconnect 和 LastError 不是泛型的,所以它们在每一个受支持的 Delphi 版本上都可用。
所有这些内容的完整示例随源码一起提供,位于 sgcBase_AsyncAwait_Examples.pas,涵盖任务、future、延迟、取消、轮询状态、并行 HTTP 请求以及上面那个超时看门狗。
可用性
任务与 future 在 sgcWebSockets 2026.7 中提供,支持 Delphi 2010 到 Delphi 13 以及 C++Builder,覆盖 Win32/Win64、Linux64、macOS、Android 和 iOS。阻塞式的 Connect、Disconnect 和 LastError 在 Delphi 7 到 13 上均可用。
拥有有效订阅的客户可以从客户专区下载新版本。试用用户可以在 esegece.com/products/websockets/download 获取更新后的安装程序。
有疑问、反馈,或者需要帮助把一个基于回调的窗体改写成任务?联系我们,回复你的将是编写这些代码的人。
