You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 19:05:22