CancellationToken:那个被90%开发者低估的性能救星

作者:微信公众号:【架构师老卢】
6-7 8:38
14

如果没人倾听,你不会继续说话。那为什么用户离开后,你的代码还在运行?

我承认,曾经我也忽视CancellationToken。心想:"这有什么用?我的代码跑得好好的,何必复杂化?"后来才发现,这种想法正在慢慢蚕食我的应用性能。

不是以惊天崩溃的方式,甚至没有任何红色警报。只是些内存泄漏、不必要的CPU占用,以及服务器为早已离开的用户继续执行任务。

最糟糕的是——这本可以通过一个小小的改变来预防。

什么是CancellationToken?

首先明确基础概念。CancellationToken是.NET中用于取消长时间运行操作的机制。它不会强制终止任何操作,只是礼貌地通知:"嘿,可以停下了,我们不再需要这个结果了。"

无论是处理Web API调用、后台服务还是密集循环,如果你不监听它,代码就会继续执行——即使早已没人等待结果。用户离开了,标签页关闭了,但你的代码?还在像忠实的仆人般继续工作。

我的血泪教训

曾经有个页面加载时会下载文件:

[HttpGet("download")]
public async Task<IActionResult> DownloadFile(string fileName)
{
    var result = await DownloadFileAsync(fileName);
    return Ok(result);
}

public async Task<string> DownloadFileAsync(string fileName)
{
    await Task.Delay(5000); // 模拟长时间下载
    return $"已下载 {fileName}";
}

起初这没什么问题,直到我发现:每次用户刷新页面或跳转时,新下载任务启动了,但旧任务呢?它依然在运行,占用内存,为空气用户卖力工作...

拯救方案

解决方法简单得令人尴尬:

[HttpGet("download")]
public async Task<IActionResult> DownloadFile(string fileName, CancellationToken token)
{
    var result = await DownloadFileAsync(fileName, token);
    return Ok(result);
}

public async Task<string> DownloadFileAsync(string fileName, CancellationToken token)
{
    try
    {
        await Task.Delay(5000, token); // 尊重取消令牌
        return $"已下载 {fileName}";
    }
    catch (OperationCanceledException)
    {
        return "下载已取消";
    }
}

ASP.NET Core早已通过HttpContext.RequestAborted提供取消令牌。你只需要使用它。现在当用户刷新或关闭标签页时,服务器知道该停止工作。

没有幽灵任务,没有内存浪费,再也不会疑惑"为什么空闲应用占用800MB内存?"

同步代码同样需要警惕

多数开发者以为取消机制仅适用于异步代码——直到我在后台服务中发现这段代码:

public void ProcessBatch()
{
    var items = Enumerable.Range(0, 1000).ToList();
    foreach (var item in items)
    {
        // 模拟CPU密集型工作
        ExpensiveComputation(item);
    }
}

即使在优雅关闭期间,它依然...继续运行。解决方案:

public void ProcessBatch(CancellationToken token)
{
    var items = Enumerable.Range(0, 1000).ToList();
    foreach (var item in items)
    {
        if (token.IsCancellationRequested)
        {
            return; // 取消时提前退出
        }
        ExpensiveComputation(item);
    }
}

现在它会定期检查取消请求,当系统发出信号时立即收拾行李离开。

为何开发者忽视它?又为何不该忽视?

有人说:"没关系,GC会清理的"
有人说:"我们的基础设施能处理额外任务"

确实——直到某天:

  • 后台作业因用户取消而不断重试,导致重复处理
  • 工作者服务关闭后挂起60秒,只因从未检查取消令牌
  • 云账单因处理废弃请求的隐形工作而暴涨

系统管理员都知道:小问题会积少成多。

CancellationToken使用清单

不必过度执着,但需保持尊重。这是我的实践清单:

  1. 为所有可能耗时的控制器动作或服务方法添加CancellationToken参数
  2. 在调用链中向下传递令牌
  3. 执行重量级操作的循环中,定期检查IsCancellationRequested
  4. 可能被取消的操作中,避免使用纯Task.Delay或Thread.Sleep
  5. 即使在开发/测试环境,也模拟用户取消来验证清理机制

这是那种在酿成大祸前就会见效的好习惯。

更深层的世界

本文只是冰山一角。

取消令牌背后还有完整体系:令牌组合、超时控制、链接令牌、后台工作者的优雅关闭、重试安全处理等。

你是否曾因忽略CancellationToken而踩坑?在评论区分享你的惊险故事,或者告诉我你想了解的高级模式——链接令牌?超时控制?还是重试逻辑?

相关留言评论
昵称:
邮箱:
阅读排行