我需要一个每25毫秒触发一次的计时器。我一直在比较Timer
dotnet核心运行时和最新的单运行时在Windows 10和Linux(Ubuntu Server 16.10和12.04)之间的默认实现。
我不太了解计时器精度方面的一些差异。
我正在使用以下代码来测试Timer:
// inside Main() var s = new Stopwatch(); var offsets = new List(); 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)));
在Windows上到处都是:
... 36 25 36 26 36 5,8875 # <-- average timing error
在Linux上使用dotnet core,到处都是:
... 25 30 27 28 27 2.59776536312849 # <-- average timing error
但是单声道Timer
非常精确:
... 25 25 24 25 25 25 0.33 # <-- average timing error
编辑:即使在Windows上,单声道仍保持其定时精度:
... 25 25 25 25 25 25 25 24 0.31
是什么造成了这种差异?与mono相比,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 EventHandlerElapsed; 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) { nextTrigger += interval; float elapsed; while (true) { elapsed = ElapsedHiRes(stopwatch); float diff = nextTrigger - elapsed; if (diff <= 0f) break; if (diff < 1f) Thread.SpinWait(10); else if (diff < 5f) Thread.SpinWait(100); else if (diff < 15f) Thread.Sleep(1); else Thread.Sleep(10); if (!isRunning) return; } float delay = elapsed - nextTrigger; Elapsed?.Invoke(this, new HiResTimerElapsedEventArgs(delay)); // 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; } }