给 .NET 网络调用设定"预算":用 CancellationTokenSource.CancelAfter 处理超时取消
网络请求没有在预期时间内完成时,调用方往往不希望无限等待。C# 的 CancellationTokenSource.CancelAfter 可以把这种等待上限表达为取消计划:到达指定时间后,它会向关联的取消令牌发出取消信号。Microsoft Learn 的示例把令牌传给 HttpClient.GetAsync 和 HttpContent.ReadAsByteArrayAsync,并在入口处捕获 OperationCanceledException。
先分清:取消信号不是"强行终止"
CancelAfter 的职责是安排取消,而不是替某个任意方法强制收尾。被调用的 API 是否能够及时结束,取决于它是否接收并观察同一个 CancellationToken。因此,下面两步应成对出现:
- 用
CancellationTokenSource生成令牌,并通过CancelAfter安排取消; - 把
cts.Token继续传给支持取消的异步 API。
一个最小的 HTTP 示例
csharp
using System.Net.Http;
static async Task<string> GetBodyAsync(Uri uri, TimeSpan timeout)
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(timeout);
try
{
using var response = await new HttpClient().GetAsync(uri, cts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new TimeoutException($"请求在 {timeout} 内未完成。");
}
}
这里的 when (cts.IsCancellationRequested) 用于区分这个调用范围内由超时计划触发的取消。是否要转换成 TimeoutException 取决于调用方的错误模型;如果上层需要统一处理取消,也可以保留原始的 OperationCanceledException。
把令牌传到实际 I/O 边界
只在外层调用 CancelAfter、却遗漏向 I/O API 传递令牌,是最常见的失配。Microsoft Learn 的完整示例在循环中调用 ProcessUrlAsync(url, client, s_cts.Token),而该方法又将 token 传给 client.GetAsync(url, token) 与 response.Content.ReadAsByteArrayAsync(token)。这样,下载尚未完成且已到取消时间时,等待中的可取消操作才有机会以取消结束。
异常处理放在调用边界
官方示例在 Main 的 try 块中设置 s_cts.CancelAfter(3500) 并等待工作方法;若取消先发生,则捕获 OperationCanceledException。这种安排把"这一批工作是否超时"的决定集中在入口处,而不需要让每个底层方法各自猜测如何展示或记录超时。
同时要注意,取消与 HTTP 成功不是一回事:GetAsync 成功返回后,仍可能得到非成功状态码,所以示例额外调用 EnsureSuccessStatusCode()。取消逻辑解决的是等待边界;状态码校验仍应按业务需要单独处理。
资源生命周期也属于实现的一部分
Microsoft Learn 的示例在 finally 中释放 CancellationTokenSource。在局部方法中使用 using 能让这一点更直接:请求成功、发生 HTTP 错误或因取消离开方法时,资源都会被释放。对于复用的 HttpClient,则应依据应用的生命周期和既有配置管理,而不是仅为一次请求随意复制全局策略。
落地检查清单
- 超时上限是否由调用方明确给出,而不是散落的魔法数字?
- 令牌是否一直传到了数据库、HTTP、文件等真正可等待的 API?
- 是否在合适的调用边界处理
OperationCanceledException? - 是否把取消、非成功 HTTP 状态和其他故障分别记录或映射?
把取消看作协作协议,而不是中断按钮,代码的控制流会更清晰:调用方声明等待预算,底层 API 观察令牌,边界层决定如何向上报告结果。