繁体   English   中英

CancellationToken不传播

[英]CancellationToken doesn't propagate

我正在尝试创建将在N秒后在某些事件上触发的函数。 如果再次出现同一事件,则应取消先前计划的执行,而应该安排新的计划。

我尝试通过以下代码对这种行为进行建模:

class Trend<T>
{
    private CancellationTokenSource cancellationToken =
        new CancellationTokenSource();

    public void AddObservation(T observation)
    {
        // I don't really care about T.
        //
        Func<CancellationToken, T, Task> action =
            async (CancellationToken token, T obs) =>
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
                if (!token.IsCancellationRequested)
                {
                    Console.WriteLine($"{DateTime.UtcNow} Task executed {obs}");
                }
            }
            catch (TaskCanceledException)
            {
            }
        };

        // cancel previos execution, if any.
        cancellationToken.Cancel();

        // create new token
        cancellationToken = new CancellationTokenSource();

        Task.Run(async () => await action(cancellationToken.Token, observation));
    }
}

简单测试:

static void Main(string[] args)
{
    Trend<int> trend = new Trend<int>();

    // Schedule 10K tasks
    for (int i = 0; i < 10; i++)
    {
        trend.AddObservation(i);
    }

    // Only the last one should execute.
    Thread.Sleep(TimeSpan.FromSeconds(10000));
}

我的期望是只执行最后一个观察,而其他所有观察都取消。 实际上,没有一项任务被取消。

现在,有效的方法是创建一个取消令牌列表并保留对每个令牌的引用:

private List<CancellationTokenSource> cancellationTokens =
    new List<CancellationTokenSource>();
...
    foreach (var ct in cancellationTokens)
    {
        ct.Cancel();
    }

    CancellationTokenSource c = new CancellationTokenSource();
    cancellationTokens.Add(c);

    Task.Run(async () => await action(c.Token, observation));

粗略的假设是垃圾收集器正在清除CancellationTokenSource,从而使Cancel调用的效果无效。

我对异步内部知识了解不多,因此可以寻求帮助。

因为所有任务都使用相同的CancellationToken

您可以通过将代码更改为此进行检查:

(此代码仅供测试。答案在此代码下方)

class Trend<T>
    {
        private CancellationTokenSource cancellationToken = new CancellationTokenSource();
        public void AddObservation(T observation)
        {
            // I don't really care about T.
            //
            Func<CancellationToken, T, Task> action = async (CancellationToken token, T obs) =>
            {
                try
                {
                    await Task.Delay(TimeSpan.FromSeconds(5), token);
                    if (!token.IsCancellationRequested)
                    {
                        Console.WriteLine($"{DateTime.UtcNow} Task executed {obs} TOKEN IS {token.GetHashCode()}" );
                    }
                }
                catch (TaskCanceledException)
                {
                }
            };

            // cancel previos execution, if any.
            cancellationToken.Cancel();

            // create new token
            cancellationToken = new CancellationTokenSource();
            Console.WriteLine($"TOKEN CREATED {cancellationToken.Token.GetHashCode()}");

            Task.Run(async () => await action(cancellationToken.Token, observation));
        }

    }

但为什么?

当您运行Task.Run(async () => await action(cancellationToken.Token, observation)); 您正在计划一个Task ,该Task将启动另一个任务,此处为action 因此,在所有任务开始执行并调用actioncancellationToken引用到最后创建的那个,因此所有Task都会通过最后创建的 CancellationToken调用操作。

只需简单地调用action(cancellationToken.Token, observation); 代替Task.Run(...) ; 因此您的操作将使用当前创建的CancellationToken调用。

解决方案完整代码

class Trend<T>
    {
        private CancellationTokenSource cancellationToken = new CancellationTokenSource();
    public void AddObservation(T observation)
    {
        // I don't really care about T.
        //
        Func<CancellationToken, T, Task> action = async (CancellationToken token, T obs) =>
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
                if (!token.IsCancellationRequested)
                {
                    Console.WriteLine($"{DateTime.UtcNow} Task executed {obs}");
                }
            }
            catch (TaskCanceledException)
            {
            }
        };

        // cancel previos execution, if any.
        cancellationToken.Cancel();

        // create new token
        cancellationToken = new CancellationTokenSource();

        action(cancellationToken.Token, observation);
    }
}

这是我的建议。 Trend实施可以接受操作,可以多次重新安排。

class Trend
{
    private readonly Action _action;
    private CancellationTokenSource _cts;
    private Task _task = Task.CompletedTask;

    public Task Completion { get => _task; }

    public Trend(Action action) { _action = action; } // Constructor

    public void CompleteAfter(int msec)
    {
        _cts?.Cancel();
        _cts = new CancellationTokenSource();
        _task = Task.Delay(msec, _cts.Token).ContinueWith(t =>
        {
            if (!t.IsCanceled) _action?.Invoke();
        }, TaskContinuationOptions.ExecuteSynchronously);
    }
}

用法示例:

Trend trend = new Trend(() =>
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Action!");
});
for (int i = 0; i < 10; i++)
{
    Thread.Sleep(100);
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Scheduling trend...");
    trend.CompleteAfter(500);
}
await trend.Completion;
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Finished");

输出:

04:39:06.541计划趋势...
04:39:06.669计划趋势...
04:39:06.771计划趋势...
04:39:06.872计划趋势...
04:39:06.990计划趋势...
04:39:07.104计划趋势...
04:39:07.205计划趋势...
04:39:07.306计划趋势...
04:39:07.408计划趋势...
04:39:07.512计划趋势...
04:39:08.027行动!
04:39:08.027完成

当前,该动作可以被多次调用。 如果不希望这样, _action在第一次调用之前将_action字段设置为null。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM