CancellationToken仅通知取消,不自动取消操作;需目标代码主动检查并响应,否则Cancel()无效。常见原因包括未传入token、未调用ThrowIfCancellationRequested()、使用不支持取消的API、同步I/O未异步化、token未向下传递等。

CancellationToken 本身不能取消任何东西,它只负责“通知”——目标代码必须主动响应,否则调用 Cancel() 就像对着空房间喊停。
为什么 await 之后任务还在跑?
常见现象是:调用 cts.Cancel() 后,await SomeAsyncMethod(token) 没抛 OperationCanceledException,甚至日志还在打印。根本原因不是 token 失效,而是方法压根没检查它。
-
SomeAsyncMethod内部没传入CancellationToken参数,或传了但没在关键路径上调用token.ThrowIfCancellationRequested() - 用了不支持取消的 API,比如直接调用
Task.Run(() => Thread.Sleep(5000))—— 这种纯同步阻塞不会响应 token - 异步方法里混了同步 I/O(如
File.ReadAllText(path)),而没替换成File.ReadAllTextAsync(path, token) - 忘了把 token 传进下一层异步调用,比如
DoWorkAsync()里调用了HttpClient.GetStringAsync(url)却没传token
CancelAfter 不生效的三个典型场景
cts.CancelAfter(3000) 看起来很省事,但实际常被误用:
- 调用
CancelAfter太晚:必须在await开始前设置,如果写在await之后或 try 块内部,就失去意义 - 目标方法根本不接受
CancellationToken:比如你对一个没参数的Task DelayAsync()调用CancelAfter,它照样睡满 5 秒 - 多个 token 混用:一个方法接收的是
cts.Token,但你却对另一个new CancellationTokenSource()调用了CancelAfter
正确姿势是:cts = new CancellationTokenSource(); cts.CancelAfter(3000); await DoSomethingAsync(cts.Token);
如何安全地取消同步阻塞操作?
第三方 SDK 或老式 API(如串口通信、某些硬件驱动)常提供阻塞式调用,无法直接接受 CancellationToken。这时不能靠轮询 IsCancellationRequested,得用 Register 注册回调:
- 注册必须在阻塞调用开始前完成,否则存在竞态:先
token.Register(() => port.Close()),再port.Read(...) - 回调里只能做“中断动作”,比如关闭句柄、设置
volatile bool _shouldExit、调用Thread.Interrupt()(慎用) - 不能在回调里 await 或抛异常——
Register是同步上下文,抛异常会静默吞掉 - 确保
CancellationTokenSource生命周期覆盖整个操作,别在注册后就Dispose()
示例:token.Register(() => { if (_process != null && !_process.HasExited) _process.Kill(); }); _process.Start();
多个任务怎么统一取消又不漏掉?
用 Task.WhenAny + token 是最轻量的“取最快结果并取消其余”的方案,但要注意陷阱:
-
WhenAny返回的 task 完成后,其他 task 仍处于 Running 状态,必须手动await它们或显式Cancel(),否则可能泄漏资源 - 别直接
await Task.WhenAny(tasks)然后忽略返回值——要拿到那个最先完成的 task,再对剩下所有调用token.Cancel() - 如果任务本身不响应 token(比如没传参或没检查),
Cancel()无效,得配合超时或进程级 kill - 避免在循环中反复 new
CancellationTokenSource,应复用或用using确保释放
真正容易被忽略的是:CancellationToken 的线程安全性只保证状态读取,不保证回调执行顺序;多个 Register 回调之间无执行顺序保障,也不保证和主线程同步。











