C# 计时器分辨率:Linux (mono, dotnet core) vs Windows



我需要一个每 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评论中提到。

最新更新