C# 计时器解析:Linux(单声道、dotnet 核心)与 Windows

C# Timer resolution: Linux (mono, dotnet core) vs Windows(C# 计时器解析:Linux(单声道、dotnet 核心)与 Windows)
本文介绍了C# 计时器解析:Linux(单声道、dotnet 核心)与 Windows的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I need a timer that fires every 25ms. I've been comparing the default Timer implementation between Windows 10 and Linux (Ubuntu Server 16.10 and 12.04) on both the dotnet core runtime and the latest mono-runtime.

计时器精度有一些我不太了解的差异.

There are some differences in the timer precision that I don't quite understand.

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

I'm using the following piece of code to test the 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)));

在 Windows 上到处都是:

On windows it's all over the place:

...
36
25
36
26
36
5,8875 # <-- average timing error

在 linux 上使用 dotnet core,到处都少了:

Using dotnet core on linux, it's less all over the place:

...
25
30
27
28
27
2.59776536312849 # <-- average timing error

但是单声道的Timer非常精确:

But the mono Timer is very precise:

...
25
25
24
25
25
25
0.33 # <-- average timing error

即使在 Windows 上,单声道仍然保持其计时精度:

Even on windows, mono still maintains its timing precision:

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

造成这种差异的原因是什么?与单声道相比,dotnet 核心运行时的处理方式是否有好处,可以证明丢失的精度是合理的?

What is causing this difference? Is there a benefit to the way the dotnet core runtime does things compared to mono, that justifies the lost precision?

推荐答案

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

Unfortunately you cannot rely on timers in the .NET framework. The best one has 15 ms frequency even if you want to trigger it in every millisecond. But you can implement a high-resolution timer with microsec precision, too.

注意:这仅在 Stopwatch.IsHighResolution 返回 true 时有效.在 Windows 中,从 Windows XP 开始就是如此.但是,我没有测试其他框架.

Note: This works only when Stopwatch.IsHighResolution returns true. In Windows this is true starting with Windows XP; however, I did not test other frameworks.

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 在评论中提到的问题.

Edit 2021: Using the latest version that does not have the issue @hankd mentions in the comments.

这篇关于C# 计时器解析:Linux(单声道、dotnet 核心)与 Windows的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!

本站部分内容来源互联网,如果有图片或者内容侵犯您的权益请联系我们删除!

相关文档推荐

DispatcherQueue null when trying to update Ui property in ViewModel(尝试更新ViewModel中的Ui属性时DispatcherQueue为空)
Drawing over all windows on multiple monitors(在多个监视器上绘制所有窗口)
Programmatically show the desktop(以编程方式显示桌面)
c# Generic Setlt;Tgt; implementation to access objects by type(按类型访问对象的C#泛型集实现)
InvalidOperationException When using Context Injection in ASP.Net Core(在ASP.NET核心中使用上下文注入时发生InvalidOperationException)
LINQ many-to-many relationship, how to write a correct WHERE clause?(LINQ多对多关系,如何写一个正确的WHERE子句?)