CancellationToken不传播

时间:2019-08-01 21:02:57

标签: c# asynchronous async-await

我正在尝试创建一个在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调用的效果无效。

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

2 个答案:

答案 0 :(得分:0)

因为所有任务都使用相同的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来启动另一个任务action。 因此在开始执行所有任务并调用action之前,cancellationToken会参考最后创建的任务,因此所有Task都会调用最后创建的操作 {{1} }。

解决方案

只需调用CancellationToken而不是action(cancellationToken.Token, observation);即可;因此您的操作将以当前创建的Task.Run(...)进行调用。

解决方案完整代码

CancellationToken

答案 1 :(得分:0)

这是我的建议。此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字段设置为null。