17370845950

c# CancellationToken.Register 的用法和注意事项
CancellationToken.Register 在 CancellationTokenSource.Cancel() 被调用或 CancelAfter() 到期后触发,仅一次且仅当 token 未被释放;回调默认同步执行于取消线程,异常会被吞掉,需 try/catch;必须显式 Unregister 或 using 管理生命周期。

CancellationToken.Register 什么时候会触发?

它只在 CancellationTokenSource.Cancel() 被调用(或 CancelAfter() 到期)后触发,且仅当该 CancellationToken 尚未被释放(比如 ctsDispose())时才安全执行。不是“注册就立刻跑”,也不是“每次轮询都调”,而是纯事件式回调——一次取消,最多触发一次(除非重复注册多个委托)。

  • 回调在取消信号发出后、异步任务真正退出前执行,常用于资源清理、日志记录、状态重置
  • 如果 token 来自已 Dispose()CancellationTokenSourceRegister 不报错但回调永远不会执行
  • 回调默认在取消发生的线程上同步执行(比如你在 UI 线程调 cts.Cancel(),回调就在 UI 线程跑),可能阻塞主线程 —— 若需异步或切线程,得自己包装 Task.Run 或用 async + await 配合 TaskScheduler

CancellationToken.Register 的参数陷阱

最常用的是 Register(Action) 重载,但它有隐藏行为:如果回调里抛异常,整个取消流程不会中断,但异常会被吞掉(.NET 默认不传播),你根本看不到错误 —— 这是线上排查“为什么清理没做”的高频盲区。

  • 务必在回调内部加 try/catch,尤其涉及文件关闭、数据库连接释放等操作
  • state 参数的重载(Register(Action, object))更安全:可传入 IDisposable 实例,避免闭包捕获导致对象生命周期延长
  • 不要传入异步 lambda(如 () => await DoCleanupAsync())—— Register 只接受同步委托,await 会被忽略,变成火种式执行(fire-and-forget),极易丢失异常和上下文

注册后怎么取消注册?

返回值 CancellationTokenRegistration 是结构体,必须显式调用 Unregister() 才能解除绑定;否则即使 token 已失效,只要 cts 没被 GC,回调仍可能被调用(尤其在反复创建/取消的循环场景中)。

  • 推荐用 using 声明(因为 CancellationTokenRegistration 实现了 IDisposable):
    using var registration = token.Register(() => Console.WriteLine("cleanup"));
  • 若注册后提前想撤回(比如条件变更不再需要清理),直接调 registration.Unregister(),它返回 bool 表示是否成功(已触发则返回 false
  • 别依赖 GC 回收自动解绑 —— CancellationTokenRegistration 不含终结器,不回收也不会泄露内存,但逻辑上容易“多清一次”或“漏清”

ThrowIfCancellationRequested() 混用要注意什么?

RegisterThrowIfCancellationRequested() 完全不同层:前者是“取消后干点啥”,后者是“我正干活,检查下要不要停”。它们可以共存,但顺序和时机很关键。

  • 如果你在 Task.Run 内部一边循环一边调 token.ThrowIfCancellationRequested(),又在外层 token.Register 注册了清理回调 —— 那么一旦触发取消,回调会在 OperationCanceledException 抛出**之后、任务彻底结束之前**执行
  • 但若你在回调里又去调 token.ThrowIfCancellationRequested(),会直接抛二次异常(因为 token 已是取消态),导致未处理异常崩溃
  • 典型误用:token.Register(() => { if (token.IsCancellationRequested) DoCleanup(); }) —— 多余,Register 触发本身就意味着已取消,不用再查
真正难的不是写对那行 Register,而是想清楚:这个清理动作,是不是必须在取消那一刻发生?有没有竞态?会不会被重复触发?要不要跨线程?这些细节不抠,上

线后就是静默失败或偶发崩溃。