我需要一个每 25 毫秒触发一次的计时器。我一直在比较 Windows 10 和 Linux(Ubuntu Server 16.10 和 12.04)在 dotnet 核心运行时和最新的单声道运行时上的默认Timer
实现。
计时器精度存在一些我不太了解的差异。
我使用以下一段代码来测试计时器:
// inside Main()
var s = new Stopwatch();
var offsets = new List<long>();
const int interval = 25;
using (var t = new Timer((obj) =>
{
offsets.Add(s.ElapsedMilliseconds);
s.Restart();
}, null, 0, interval))
{
s.Start();
Thread.Sleep(5000);
}
foreach(var n in offsets)
{
Console.WriteLine(n);
}
Console.WriteLine(offsets.Average(n => Math.Abs(interval - n)));
在窗户上,到处都是:
...
36
25
36
26
36
5,8875 # <-- average timing error
在 Linux 上使用 dotnet 核心,它到处都是:
...
25
30
27
28
27
2.59776536312849 # <-- average timing error
但单声道Timer
非常精确:
...
25
25
24
25
25
25
0.33 # <-- average timing error
编辑:即使在窗口上,单声道仍然保持其计时精度:
...
25
25
25
25
25
25
25
24
0.31
是什么导致了这种差异?与单声道相比,dotnet 核心运行时的执行方式是否有好处,从而证明丢失的精度是合理的?
您不能依赖 .NET 框架中的计时器。最好的频率为 15 毫秒,即使您想每毫秒触发一次。但是,您也可以实现具有微秒精度的高分辨率计时器。
注意:这仅在Stopwatch.IsHighResolution
返回 true 时才有效。在Windows中,从Windows XP开始就是如此;但是,我没有测试其他框架。
public class HiResTimer
{
// The number of ticks per one millisecond.
private static readonly float tickFrequency = 1000f / Stopwatch.Frequency;
public event EventHandler<HiResTimerElapsedEventArgs> Elapsed;
private volatile float interval;
private volatile bool isRunning;
public HiResTimer() : this(1f)
{
}
public HiResTimer(float interval)
{
if (interval < 0f || Single.IsNaN(interval))
throw new ArgumentOutOfRangeException(nameof(interval));
this.interval = interval;
}
// The interval in milliseconds. Fractions are allowed so 0.001 is one microsecond.
public float Interval
{
get { return interval; }
set
{
if (value < 0f || Single.IsNaN(value))
throw new ArgumentOutOfRangeException(nameof(value));
interval = value;
}
}
public bool Enabled
{
set
{
if (value)
Start();
else
Stop();
}
get { return isRunning; }
}
public void Start()
{
if (isRunning)
return;
isRunning = true;
Thread thread = new Thread(ExecuteTimer);
thread.Priority = ThreadPriority.Highest;
thread.Start();
}
public void Stop()
{
isRunning = false;
}
private void ExecuteTimer()
{
float nextTrigger = 0f;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
while (isRunning)
{
float intervalLocal = interval;
nextTrigger += intervalLocal;
float elapsed;
while (true)
{
elapsed = ElapsedHiRes(stopwatch);
float diff = nextTrigger - elapsed;
if (diff <= 0f)
break;
if (diff < 1f)
Thread.SpinWait(10);
else if (diff < 10f)
Thread.SpinWait(100);
else
{
// By default Sleep(1) lasts about 15.5 ms (if not configured otherwise for the application by WinMM, for example)
// so not allowing sleeping under 16 ms. Not sleeping for more than 50 ms so interval changes/stopping can be detected.
if (diff >= 16f)
Thread.Sleep(diff >= 100f ? 50 : 1);
else
{
Thread.SpinWait(1000);
Thread.Sleep(0);
}
// if we have a larger time to wait, we check if the interval has been changed in the meantime
float newInterval = interval;
if (intervalLocal != newInterval)
{
nextTrigger += newInterval - intervalLocal;
intervalLocal = newInterval;
}
}
if (!isRunning)
return;
}
float delay = elapsed - nextTrigger;
if (delay >= ignoreElapsedThreshold)
{
fallouts += 1;
continue;
}
Elapsed?.Invoke(this, new HiResTimerElapsedEventArgs(delay, fallouts));
fallouts = 0;
// restarting the timer in every hour to prevent precision problems
if (stopwatch.Elapsed.TotalHours >= 1d)
{
stopwatch.Restart();
nextTrigger = 0f;
}
}
stopwatch.Stop();
}
private static float ElapsedHiRes(Stopwatch stopwatch)
{
return stopwatch.ElapsedTicks * tickFrequency;
}
}
public class HiResTimerElapsedEventArgs : EventArgs
{
public float Delay { get; }
internal HiResTimerElapsedEventArgs(float delay)
{
Delay = delay;
}
}
编辑 2021:使用没有问题的最新版本,@hankd评论中提到。