CancellationToken 仅发信号,不自动取消代码;任务是否停止取决于是否在关键位置检查并响应它。常见问题包括漏传 token、使用不可取消的同步 API、未在循环中轮询、未透传 token 到底层异步调用、注册回调未释放等。

CancellationToken 不会自动取消任何代码,它只负责发信号;任务是否停止,完全取决于你有没有在关键位置检查并响应它。
为什么调用 Cancel() 后任务还在跑
常见现象是:点击“取消”按钮后,await DoWorkAsync(token) 没抛 OperationCanceledException,日志照打、循环照转。根本原因不是 token 失效,而是目标代码压根没响应。
- 方法签名里声明了
CancellationToken token,但内部调用Task.Delay(5000)时漏传了token - 用了不支持取消的同步 API,比如
Thread.Sleep(5000)、File.ReadAllText(path)—— 这些无法被中断 - 异步链中断:例如
DoWorkAsync()调用了HttpClient.GetStringAsync(url),却没传token - 只在方法开头检查一次
token.IsCancellationRequested,后续长循环或大块计算中再没轮询
如何让自定义异步方法真正支持取消
所有可取消的 .NET 异步原语(如 Task.Delay、HttpClient.GetAsync、Stream.ReadAsync)都接受 CancellationToken 参数,必须显式传入。封装自己的方法时,也要把 CancellationToken 作为参数透传到底层调用链。
- 正确写法:
await Task.Delay(5000, token)、await httpClient.GetAsync("https://api.example.com", token) - 错误写法:
await Task.Run(() => Thread.Sleep(5000))—— 这只是把阻塞扔进线程池,token对它完全透明 - 循环体中必须插入检查点:
for (int i = 0; i - 避免
Thread.Sleep,改用await Task.Delay(ms, token)
CancellationTokenSource.CancelAfter() 为什么经常不生效
CancelAfter 是基于 Timer 的延迟回调,不保证毫秒级精度,更关键的是:它依赖 CancellationTokenSource 实例在整个操作生命周期内保持存活。
- 调用
CancelAfter(3000)太晚:必须在await开始前设置,写在try块内部或await之后就失去意义 - 目标方法根本不接受
CancellationToken:比如你对一个无参的DelayAsync()调用CancelAfter,它照样睡满 5 秒 -
CancellationTokenSource提前被Dispose():常见于using块中创建并调用CancelAfter,资源立即释放,回调永远不会执行 - 推荐写法:
var cts = new CancellationTokenSource(); cts.CancelAfter(3500); await DoSomethingAsync(cts.Token);,并在finally中统一cts.Dispose()
怎么处理不支持取消的同步阻塞调用
第三方 SDK、串口通信、Process.WaitForExit() 等纯阻塞调用,无法直接接受 CancellationToken。这时得靠 token.Register() 注册回调,在取消触发时执行“中断动作”。
- 注册必须在阻塞调用开始前完成,否则存在竞态:先
token.Register(() => process.Kill()),再process.WaitForExit() - 回调里只能做同步、无
await、不抛异常的操作,比如Close()、Kill()、设置volatile bool _shouldStop = true - 回调运行在线程池线程上,注意线程安全;如果要更新 UI,需手动调度回主线程
- 别忘了保存
token.Register()返回的IDisposable,并在操作结束时Dispose()解绑,否则可能内存泄漏
最常被忽略的一点是:CancellationTokenSource 实例不能复用,也不能跨 async 方法边界长期持有引用;每个独立任务都应使用新的实例,且生命周期必须覆盖整个操作过程——漏掉这点,取消逻辑就形同虚设。


















