异步调用先传取消
C# 项目里 async/await 让异步代码看起来像同步代码,但这不代表生命周期也自动变简单。用户关闭页面、请求超时、服务停止、后台任务取消,如果 CancellationToken 没有传到底,代码表面返回了,实际工作可能还在继续。
这篇文章不打算把取消令牌传递讲成抽象原则,而是放到真实项目里拆:它受哪些机制影响,哪里需要提前定责,失败以后怎样恢复。我的取舍是,先让状态和责任可解释,再追求漂亮的封装或更快的路径。
入口 token 不能只摆样子
ASP.NET 请求入口通常能拿到 RequestAborted,但很多代码只在控制器里接住,没有继续传给服务层和仓储层。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
在机制上,CancellationToken、HttpClient、数据库调用、后台队列、超时和清理会一起影响取消令牌传递。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:从入口方法开始,把 token 传到每个可能等待的调用。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:只在最外层判断取消,内部 IO 仍会继续跑。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
我会用一个朴素标准判断这一段是否完成:请求取消后,下游 IO、队列任务和日志都能体现取消原因。如果团队回答这个问题时还要靠猜,说明取消令牌传递仍然停留在口头规则,没有真正进入实现和验收。
这张图只画和“入口 token 不能只摆样子”直接相关的路径,重点是让边界、状态和失败出口都能被看见。
IO 调用要真正使用 token
HttpClient、数据库、文件读写如果支持 CancellationToken,就应该传进去。否则取消只是业务代码停止等待,下游资源还在消耗。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
放到“IO 调用要真正使用 token”这一节,这些机制不是背景板,CancellationToken、HttpClient、数据库调用、后台队列、超时和清理会一起影响取消令牌传递。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:检查每个 await 调用是否有 token 参数。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:方法签名带 token,但调用时传 CancellationToken.None,等于断链。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
对“IO 调用要真正使用 token”来说,验收标准可以更具体一点:请求取消后,下游 IO、队列任务和日志都能体现取消原因。如果团队回答这个问题时还要靠猜,说明取消令牌传递仍然停留在口头规则,没有真正进入实现和验收。
针对“IO 调用要真正使用 token”,可以把检查清单压成三项:先确认对象是谁,再确认它的生命周期在哪里结束,最后确认失败以后谁负责接手。清单越短,越能逼出真正关键的规则。
后台队列要区分停止和取消
服务停机时的取消,和用户取消某个任务,不是同一种语义。后台队列需要知道是全局停止还是单任务撤销。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
放到“后台队列要区分停止和取消”这一节,这些机制不是背景板,CancellationToken、HttpClient、数据库调用、后台队列、超时和清理会一起影响取消令牌传递。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:为队列项保存 taskId 和对应 token 来源。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:把所有取消都当失败,会污染任务统计。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
对“后台队列要区分停止和取消”来说,验收标准可以更具体一点:请求取消后,下游 IO、队列任务和日志都能体现取消原因。如果团队回答这个问题时还要靠猜,说明取消令牌传递仍然停留在口头规则,没有真正进入实现和验收。
这张图只画和“后台队列要区分停止和取消”直接相关的路径,重点是让边界、状态和失败出口都能被看见。
下面这段只作为边界表达示例,不建议脱离业务直接复制:
- await client.GetAsync(url, cancellationToken);
- await repository.SaveAsync(entity, cancellationToken);
清理逻辑不要被取消打断
取消后仍然可能需要释放锁、写状态、记录日志。清理动作要谨慎使用同一个 token,避免还没清理就被取消。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
放到“清理逻辑不要被取消打断”这一节,这些机制不是背景板,CancellationToken、HttpClient、数据库调用、后台队列、超时和清理会一起影响取消令牌传递。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:关键清理可以使用短独立超时。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:把 cleanup 也绑定请求 token,可能导致状态无法落库。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
对“清理逻辑不要被取消打断”来说,验收标准可以更具体一点:请求取消后,下游 IO、队列任务和日志都能体现取消原因。如果团队回答这个问题时还要靠猜,说明取消令牌传递仍然停留在口头规则,没有真正进入实现和验收。
针对“清理逻辑不要被取消打断”,可以把检查清单压成三项:先确认对象是谁,再确认它的生命周期在哪里结束,最后确认失败以后谁负责接手。清单越短,越能逼出真正关键的规则。
测试要主动取消请求
取消链路不测就很难发现。用例里要模拟客户端断开、超时和服务停机,看每一层是否停得住。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
放到“测试要主动取消请求”这一节,这些机制不是背景板,CancellationToken、HttpClient、数据库调用、后台队列、超时和清理会一起影响取消令牌传递。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:为长耗时接口写取消测试。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:只测试正常 await 完成,会隐藏资源浪费。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
对“测试要主动取消请求”来说,验收标准可以更具体一点:请求取消后,下游 IO、队列任务和日志都能体现取消原因。如果团队回答这个问题时还要靠猜,说明取消令牌传递仍然停留在口头规则,没有真正进入实现和验收。
取消不是失败统计里的普通错误
取消请求不应该简单归类成失败。用户主动离开、网关超时、服务停机、业务撤销,它们都可能表现为取消,但含义完全不同。
日志里最好区分 cancelReason。否则你会在监控里看到一堆失败,却不知道哪些是用户主动取消,哪些是真的处理能力不足。
验收时要看两个结果:调用链是否停止无意义工作,必要清理是否仍然完成。能同时满足这两点,CancellationToken 才算真正传到了设计里。
方法签名要表达取消意图
CancellationToken 加到方法签名里以后,也要形成团队约定:有异步等待、有外部 IO、有长循环,就应该接收 token。否则有些方法传,有些方法不传,链路会在中间断掉。
评审时可以从入口往下追一遍 token。如果追到某层消失,就要问清楚那里是真的不需要取消,还是只是忘了继续传。
异步调用先传取消的验收样本
最后还要补一组验收样本。样本不需要覆盖所有情况,但要覆盖正常、异常、边界和恢复。对“异步调用先传取消”来说,只有这四类样本都能解释,文章里的建议才不是停留在原则层面。
我会特别关注恢复样本:失败后状态是否可追,下一次是否能继续,重复执行是否安全。很多系统不是败在第一次失败,而是败在失败后的第二次处理。
最后用三个问题收住
第一,谁拥有取消令牌传递的最终解释权。没有 owner 的规则,短期靠人记,长期靠运气。
第二,失败以后系统会留下什么证据。证据不是越多越好,而是要能回答:发生在什么条件下、处理到哪一步、下一次应该从哪里恢复。
第三,这个方案适合哪些场景、不适合哪些场景。取消传递会让方法签名变长,但能避免无意义工作继续占用资源。,所以不要把一次有效实践包装成万能答案。
如果这三个问题能说清,取消令牌传递就不再只是一个技术点,而会变成团队协作中的稳定边界。后续无论换框架、换人员还是换业务规模,都能沿着这条边界继续调整。
