CancellationToken仅发信号,取消逻辑由开发者代码实现;Task.Delay响应取消因内部检查token,而CPU密集型循环需手动插入ThrowIfCancellationRequested;CTS必须存活至异步操作完全结束,避免过早Dispose;共用token时子任务须显式接收并传递;CancelAfter需在await前设置且token被实际使用;取消是协作式退出,不可吞掉OperationCanceledException。

CancellationToken 本身不会取消任何东西,它只负责发信号;真正决定任务是否停止、何时停止、如何清理的,是你写的代码是否检查并响应它。
为什么 await Task.Delay(5000) 被 CancelAfter(3000) 取消了,但自己写的 for 循环没反应
因为 Task.Delay 内部主动检查了传入的 CancellationToken,并在超时前抛出 OperationCanceledException;而你手写的 CPU 密集型循环(比如处理数组、计算哈希)默认完全不看 token,除非你手动插入检查点。
- 必须在循环体开头、大块计算之间、I/O 操作前后显式调用
token.ThrowIfCancellationRequested()或判断token.IsCancellationRequested - 不要依赖“await 就会自动响应”——只有那些签名里带
CancellationToken参数且真正实现检查的异步方法才支持,比如HttpClient.GetStringAsync(url, token);而Thread.Sleep(5000)或纯for循环根本不接受 token - 如果循环里有
await,也要确保每个await都传了 token,例如await Task.Delay(100, token),否则等待阶段就收不到取消信号
CancellationTokenSource.Dispose() 过早调用导致取消失效
常见现象:调用了 cts.Cancel() 却没触发 OperationCanceledException,或者 token.Register() 的回调从不执行。根本原因往往是 CancellationTokenSource 在任务还在运行时就被 Dispose() 了。
-
CancellationTokenSource必须存活到整个异步操作生命周期结束——包括所有await完成、异常处理完毕、资源清理完成 - 别在
using (var cts = new CancellationTokenSource()) { ... }里包裹整个异步调用,因为using块一退出就 Dispose,而此时await可能还没回来 - 正确做法是把
cts提升为字段或局部变量,在finally或明确的清理时机再Dispose()
多个异步任务共用一个 CancellationToken 但部分不响应
典型场景:用 Task.WhenAll(tasks) 并发执行一组任务,其中几个立刻返回,另几个卡住;调用 cts.Cancel() 后,只有部分任务真正退出。
- 问题往往出在子任务没收到 token——比如封装的
DoWorkAsync()方法签名没带CancellationToken参数,或调用时漏传了 - 即使父方法传了 token,子方法内部若调用了不支持取消的 API(如未传 token 的
HttpClient.PostAsync()),那一路下去都不响应 - 避免“伪传递”:不要只在顶层声明
async Task DoWorkAsync(CancellationToken token = default),却在内部写return await Task.Run(() => { /* 同步代码 */ })——Task.Run不监听 token,你得在委托内部手动检查
超时取消用 CancelAfter 但始终不生效
最常踩的坑是 CancelAfter 调用时机不对:它必须在 await 开始前设置,且关联的 token 必须被实际传入正在执行的异步路径中。
- 错误写法:
await DoSomethingAsync(); cts.CancelAfter(3000);—— 此时操作早已开始,CancelAfter 失去意义 - 正确顺序:
var cts = new CancellationTokenSource(); cts.CancelAfter(3000); await DoSomethingAsync(cts.Token); - 注意:如果
DoSomethingAsync内部又创建了新的CancellationTokenSource或用了defaulttoken,那外部的CancelAfter也无效 - 更稳妥的替代方案是直接用构造函数:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
最容易被忽略的一点:取消不是“中断执行”,而是“协作退出”。哪怕你每行都加了 token.ThrowIfCancellationRequested(),如果 catch (Exception) 把 OperationCanceledException 吞掉了,上层永远收不到取消通知——该释放的资源不释放,该标记的状态不更新,后台任务就真成泄露了。


















