CancellationToken注册委托未触发:.NET版本差异及优化方案问询
CancellationToken注册回调在Task.Delay传令牌时未执行的问题
问题现象
异步方法接收CancellationToken并注册回调委托,将令牌传入Task.Delay并等待时,令牌被取消后注册的委托始终不执行;但如果不将令牌传入Task.Delay,仅在延迟结束后调用ct.ThrowIfCancellationRequested(),委托则会正常执行。
该问题存在于.NET Framework 4.7.2、4.8及.NET 6中,.NET 7中表现符合预期(回调正常触发)。
核心原因
问题源于**using块的生命周期与回调执行时机的竞态条件**:
- 当
Task.Delay(100, ct)检测到令牌取消时,会立即抛出OperationCanceledException,异步执行流直接退出using块,触发CancellationTokenRegistration的Dispose方法,直接注销了回调委托。 - 而
CancellationToken的回调是异步调度执行的,此时回调还未被调度执行就已被注销,导致永远不会触发。 - 在不传递令牌给
Task.Delay的场景中,Task.Delay会正常完成,之后调用ct.ThrowIfCancellationRequested()抛出异常,此时using块的Dispose在异常抛出后执行,回调已有足够时间被调度并执行。
解决方案
方案1:同步等待回调执行完成(适合必须保留注册回调的场景)
通过同步原语确保在注销回调前,已触发的回调能执行完成:
using System; using System.Threading; using System.Threading.Tasks; public class Foo { public async Task Run(CancellationToken ct) { var resetEvent = new ManualResetEventSlim(true); void CancelCallbackWrapper() { resetEvent.Reset(); try { OnCancel(); } finally { resetEvent.Set(); } } using var registration = ct.Register(CancelCallbackWrapper); try { await Task.Delay(100, ct).ConfigureAwait(false); } finally { // 如果令牌已取消,等待回调执行完成后再注销 if (ct.IsCancellationRequested) { resetEvent.Wait(); } } } private void OnCancel() { Console.WriteLine("OnCancel()"); } }
方案2:在取消异常分支中执行逻辑(推荐)
如果回调仅用于响应令牌取消,直接在OperationCanceledException的处理分支中执行逻辑,完全避免竞态:
using System; using System.Threading; using System.Threading.Tasks; public class Foo { public async Task Run(CancellationToken ct) { try { await Task.Delay(100, ct).ConfigureAwait(false); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { OnCancel(); throw; // 保留取消异常的传播,不破坏原有逻辑 } } private void OnCancel() { Console.WriteLine("OnCancel()"); } }
方案3:使用UnsafeRegister(谨慎使用)
UnsafeRegister不会在Dispose时阻止已调度的回调执行,但它跳过了线程安全检查,需要自行管理回调的生命周期,仅适合特殊场景:
using System; using System.Threading; using System.Threading.Tasks; public class Foo { public async Task Run(CancellationToken ct) { var registration = ct.UnsafeRegister(_ => OnCancel(), null); try { await Task.Delay(100, ct).ConfigureAwait(false); } finally { registration.Dispose(); } } private void OnCancel() { Console.WriteLine("OnCancel()"); } }
.NET 7的修复说明
.NET 7中优化了CancellationTokenRegistration.Dispose的实现逻辑,确保已经被调度的回调会完成执行,因此该版本中不会出现此竞态问题。
内容的提问来源于stack exchange,提问作者tukra
相关产品推荐
相关产品推荐

