我正在尝试创建一个在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调用的效果无效。
我对异步内部机制了解不多,因此可以寻求帮助。
答案 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。