C# 计时器分辨率:Linux(单声道、dotnet core)与 Windows

2023-11-28

我需要一个每 25 毫秒触发一次的计时器。我一直在比较默认值Timer在 dotnet core 运行时和最新的 mono 运行时上,在 Windows 10 和 Linux(Ubuntu Server 16.10 和 12.04)之间实现。

计时器精度存在一些差异,我不太明白。

我正在使用以下代码来测试计时器:

// 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)));

在 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

Edit:即使在 Windows 上,mono 仍然保持其计时精度:

...
25
25
25
25
25
25
25
24
0.31

是什么造成了这种差异?与 mono 相比,dotnet core 运行时的处理方式是否有好处,可以证明损失的精度是合理的?


不幸的是,您不能依赖 .NET 框架中的计时器。最好的频率是 15 毫秒,即使您想每毫秒触发一次。但您也可以实现微秒精度的高分辨率计时器。

Note:这仅在以下情况下有效Stopwatch.IsHighResolution返回真。在 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 在评论中提到的问题。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C# 计时器分辨率:Linux(单声道、dotnet core)与 Windows 的相关文章

随机推荐