我的建议是避免为此使用 Timer 类。造成这种情况的原因正是你现在处理这个问题时遇到的问题。
System.Threading.Timer 类是可重入的,这意味着如果在上一个触发器调用返回之前间隔过去,即使它已经在执行,它也会再次调用该方法。您需要以一种使代码有点混乱的方式来处理方法和触发器之间的协作。
但是,让我们重新考虑一下这个问题。
你想要
- 按时间间隔调用方法
- 如果某个方法调用的执行时间比间隔时间长,则立即再次触发
更好的方法是省去触发器并编写自己的使用任务。
这是一个非常简单的例子:
public static async Task CallPeriodicAsync(Func<CancellationToken, Task> func,
int intervalMilliseconds, CancellationToken cancellationToken)
{
while (true)
{
var delay = Task.Delay(intervalMilliseconds, cancellationToken);
await func(cancellationToken);
await delay;
}
}
你可以这样开始:
CancellationToken cancellationToken = ...;
CallPeriodicAsync(async ct =>
{
int timeToRun = 1000 + r.Next(14000);
Console.WriteLine($"This time running for {timeToRun} ms");
await Task.Delay(timeToRun, ct);
}, 10000, cancellationToken);
示例输出将是(我在 CallPeriod 方法中也有一些 Console.WriteLine 来说明它在这次运行中是等待还是立即触发):
This time running for 12162 ms
trigger again immediately
This time running for 14706 ms
trigger again immediately
This time running for 12756 ms
trigger again immediately
This time running for 2187 ms
delay until next is 7813 ms
This time running for 5221 ms
delay until next is 4767 ms
This time running for 8866 ms
基本上这个 CallPeriod 方法会调用你的方法并给它 X 毫秒来完成。如果它完成得更快,它将为剩余部分增加一个延迟,如果它完成得较慢,它将重新开始循环。
这意味着我关于“保持原始间隔”的问题仍然相关。
例如,保持原始间隔会导致这种触发(---* 只是时间线),每 4 秒一次:
v v v v v
*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*
[...] [..................][.......] [...] [....
而我上面的解决方案会在延迟后扭曲循环:
v v v v v
*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*
[...] [..................][.......] [...] [....
v ^
+-- because we got -+
one second delayed
here we're now one
second late always