【问题标题】:High resolution timer in C#C#中的高分辨率计时器
【发布时间】:2014-07-19 10:02:07
【问题描述】:

是否有一个高分辨率计时器,每次计时器经过时都会引发一个事件,就像System.Timer 类一样?我需要一个高分辨率计时器来Elapse 每毫秒。

我经常看到一些帖子解释说秒表可以测量高分辨率,但我不想测量时间,我想创建一个 1 毫秒的间隔。

.NET 中有什么东西还是我要编写自己的高分辨率计时器?

【问题讨论】:

  • 当你使用 winforms 时,你的工具箱中确实有一个计时器。您可以将其间隔设置为 1 毫秒。你还需要什么?
  • 你到底想每毫秒做什么一次?一毫秒并不是很多时间。
  • @MatinLotfaliee 我需要它在 1ms 的时间间隔内实际起作用,但它不需要。
  • @nvoigt,在我们的机器上模拟真实的总线系统,节拍时间为1ms

标签: c# timer high-resolution-clock


【解决方案1】:

据我所知,.NET 框架中没有内置任何内容。 Windows 具有通过Multimedia Timer API 处理高分辨率计时器事件的机制。下面是我制作的一个简单示例,它似乎可以完成这项工作。 here似乎也有一个很好的例子。

我会注意到,此 API 会更改系统范围的设置,从而降低系统性能,因此请买家注意。出于测试目的,我建议跟踪计时器触发的频率,以验证计时是否与您尝试模拟的设备相似。由于 Windows 不是实时操作系统,系统上的负载可能会导致 MM 计时器延迟,从而导致 100 毫秒的间隔,其中包含 100 个快速连续的事件,而不是 100 个间隔 1 毫秒的事件。 MM 计时器上的一些 additional reading。

class Program
{
    static void Main(string[] args)
    {
        TestThreadingTimer();
        TestMultimediaTimer();
    }

    private static void TestMultimediaTimer()
    {
        Stopwatch s = new Stopwatch();
        using (var timer = new MultimediaTimer() { Interval = 1 })
        {
            timer.Elapsed += (o, e) => Console.WriteLine(s.ElapsedMilliseconds);
            s.Start();
            timer.Start();
            Console.ReadKey();
            timer.Stop();
        }
    }

    private static void TestThreadingTimer()
    {
        Stopwatch s = new Stopwatch();
        using (var timer = new Timer(o => Console.WriteLine(s.ElapsedMilliseconds), null, 0, 1))
        {
            s.Start();
            Console.ReadKey();
        }
    }

}

public class MultimediaTimer : IDisposable
{
    private bool disposed = false;
    private int interval, resolution;
    private UInt32 timerId; 

    // Hold the timer callback to prevent garbage collection.
    private readonly MultimediaTimerCallback Callback;

    public MultimediaTimer()
    {
        Callback = new MultimediaTimerCallback(TimerCallbackMethod);
        Resolution = 5;
        Interval = 10;
    }

    ~MultimediaTimer()
    {
        Dispose(false);
    }

    public int Interval
    {
        get
        {
            return interval;
        }
        set
        {
            CheckDisposed();

            if (value < 0)
                throw new ArgumentOutOfRangeException("value");

            interval = value;
            if (Resolution > Interval)
                Resolution = value;
        }
    }

    // Note minimum resolution is 0, meaning highest possible resolution.
    public int Resolution
    {
        get
        {
            return resolution;
        }
        set
        {
            CheckDisposed();

            if (value < 0)
                throw new ArgumentOutOfRangeException("value");

            resolution = value;
        }
    }

    public bool IsRunning
    {
        get { return timerId != 0; }
    }

    public void Start()
    {
        CheckDisposed();

        if (IsRunning)
            throw new InvalidOperationException("Timer is already running");

        // Event type = 0, one off event
        // Event type = 1, periodic event
        UInt32 userCtx = 0;
        timerId = NativeMethods.TimeSetEvent((uint)Interval, (uint)Resolution, Callback, ref userCtx, 1);
        if (timerId == 0)
        {
            int error = Marshal.GetLastWin32Error();
            throw new Win32Exception(error);
        }
    }

    public void Stop()
    {
        CheckDisposed();

        if (!IsRunning)
            throw new InvalidOperationException("Timer has not been started");

        StopInternal();
    }

    private void StopInternal()
    {
        NativeMethods.TimeKillEvent(timerId);
        timerId = 0;
    }

    public event EventHandler Elapsed;

    public void Dispose()
    {
        Dispose(true);
    }

    private void TimerCallbackMethod(uint id, uint msg, ref uint userCtx, uint rsv1, uint rsv2)
    {
        var handler = Elapsed;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    private void CheckDisposed()
    {
        if (disposed)
            throw new ObjectDisposedException("MultimediaTimer");
    }

    private void Dispose(bool disposing)
    {
        if (disposed)
            return;
        
        disposed = true;
        if (IsRunning)
        {
            StopInternal();
        }
        
        if (disposing)
        {
            Elapsed = null;
            GC.SuppressFinalize(this);
        }
    }
}

internal delegate void MultimediaTimerCallback(UInt32 id, UInt32 msg, ref UInt32 userCtx, UInt32 rsv1, UInt32 rsv2);

internal static class NativeMethods
{
    [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeSetEvent")]
    internal static extern UInt32 TimeSetEvent(UInt32 msDelay, UInt32 msResolution, MultimediaTimerCallback callback, ref UInt32 userCtx, UInt32 eventType);

    [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeKillEvent")]
    internal static extern void TimeKillEvent(UInt32 uTimerId);
}

【讨论】:

  • 这是开箱即用的。我在带有属性的生产代码中使用它。唯一的问题是它会崩溃,除非你自己创建回调委托并使用 GCHandle.Alloc() 来防止它被移动或删除......
  • 我想我遇到了这个确切的问题——在类型为... MultimediaTimerCallback::Invoke 的垃圾收集委托上进行了回调。您能否提供更多有关您如何解决此问题的见解?
  • 我相信我已经修复了与定时器回调被 GC 相关的崩溃问题。
  • 我已经在GitHub 上发布了代码,以后有任何问题都可以在那里提出。
【解决方案2】:

我无法让 Mike 的解决方案正常工作,并根据这篇 codeproject 文章 https://www.codeproject.com/Articles/17474/Timer-surprises-and-how-to-avoid-them 围绕 Windows 多媒体计时器创建了一个基本包装器

public class WinMMWrapper
{
    [DllImport("WinMM.dll", SetLastError = true)]
    public static extern uint timeSetEvent(int msDelay, int msResolution,
        TimerEventHandler handler, ref int userCtx, int eventType);

    public delegate void TimerEventHandler(uint id, uint msg, ref int userCtx,
        int rsv1, int rsv2);

    public enum TimerEventType
    {
        OneTime = 0,
        Repeating = 1
    }

    private readonly Action _elapsedAction;
    private readonly int _elapsedMs;
    private readonly int _resolutionMs;
    private readonly TimerEventType _timerEventType;
    private readonly TimerEventHandler _timerEventHandler;

    public WinMMWrapper(int elapsedMs, int resolutionMs, TimerEventType timerEventType, Action elapsedAction)
    {
        _elapsedMs = elapsedMs;
        _resolutionMs = resolutionMs;
        _timerEventType = timerEventType;
        _elapsedAction = elapsedAction;
        _timerEventHandler = TickHandler;
    }

    public uint StartElapsedTimer()
    {
        var myData = 1; //dummy data
        return timeSetEvent(_elapsedMs, _resolutionMs / 10, _timerEventHandler, ref myData, (int)_timerEventType);
    }

    private void TickHandler(uint id, uint msg, ref int userctx, int rsv1, int rsv2)
    {
        _elapsedAction();
    }
}

这是一个如何使用它的示例

class Program
{
    static void Main(string[] args)
    {
        var timer = new WinMMWrapper(100, 25, WinMMWrapper.TimerEventType.Repeating, () =>
        {
            Console.WriteLine($"Timer elapsed {DateTime.UtcNow:o}");
        });

        timer.StartElapsedTimer();

        Console.ReadKey();
    }
}

输出如下所示

2021-11-19 更新:根据 chris 的评论添加 TimerEventHandler 班级成员。

【讨论】:

  • 很好的答案,而且效果很好。我发现的一件事是TimerEventHandler 正在 WinMMWrapper 中收集垃圾。这可以通过将处理程序设置为类级别变量,然后将其传递给timeSetEvent 来解决。我想这是一个常见的非托管代码问题。
  • 您好,计时器和分辨率一样好用。但它会随着垃圾收集器而消失。我该如何预防?
【解决方案3】:

有一个选项:使用Thread.Sleep(0)。尝试调用Thread.Sleep(1) 或使用System.Threading.Timer 将始终归结为系统计时器分辨率。取决于一个可能不是最好的主意,最终您的应用程序可能只是不允许从 winmm.dll 调用 timeBeginPeriod(...)。

以下代码在我的开发机器 (i7q) 上可以解析到 +/- 10ns (0.10ms) 并且可能更高。它会给你的一个 CPU 内核带来坚实的负载,从而将其使用率提高到 100%。不会发生实际的操作系统减速,代码会通过尽早调用 Thread.Sleep 放弃大部分 CPU 时间量:

var requiredDelayMs = 0.1;
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
while (true)
{
    if (sw.Elapsed.TotalMilliseconds >= requiredDelayMs) 
    {
      // call your timer routine
    }
    Thread.Sleep(0); // setting at least 1 here would involve a timer which we don't want to
}

更全面的实现见my other answer

【讨论】:

  • Thread.Yield() 将减少上下文切换的机会。 Thread.Sleep(0) 导致上下文切换到较低优先级的进程。提升当前进程优先级将减少Thread.Yield() 自身上下文切换的机会。
  • 此外,SpinWait 可用于将所有内容打包在一起,甚至减少 Yield 上下文切换的机会:“它经过精心实施,可为一般情况提供正确的旋转行为,并且会自行启动如果它旋转足够长的时间(大约是内核转换所需的时间长度),则上下文切换。"
【解决方案4】:

Precision-Timer.NET

https://github.com/HypsyNZ/Precision-Timer.NET

一个高精度 .NET 计时器,不会杀死您的 CPU 或收集垃圾。

它的设计与任何其他 .NET 计时器一样易于使用。

用法

使用 Precision Timer 的方式与使用任何其他 Windows Timer 的方式相同。

       private PrecisionTimer PT;

       public PrecisionTimer SetInterval(Action TimerTask, int Interval)
       {
           PT = new PrecisionTimer();
           PT.Tick += (sender, args) => { TimerTask(); };
           PT.AutoReset = true;
           PT.Period = Interval;
           PT.Resolution = 0;
           PT.Start();
           return PT;
       } 

打勾

 PT.Tick += (sender, args) => { TimerTask(); };

Interval 结束时将触发的 TimerEventCallback

期间

 PT.Period = Interval;

获取或设置Tick Events、Milliseconds 之间的时间。

分辨率

 PT.Resolution = 0;

分辨率以毫秒为单位。

Resolution 随着值的减小而增加。

分辨率为 0 表示周期性事件应以尽可能高的精度发生。

但是,为了减少系统开销,您应该使用适合您的应用程序的最大值。

.Net 定时器的正常分辨率约为 12-15 毫秒,但这取决于您的主板高精度事件定时器 (HPET) 和可编程中断控制器 (PIC)

自动重置

 PT.AutoReset = true;

True 如果 Precision Timer 应该在每次间隔过去时引发 Ticks 事件。

False 如果Precision Timer 应该在第一次间隔结束后只引发一次事件。

多媒体定时器

Precision Timer.NET 是一个多媒体定时器,你可以在MSDN阅读更多关于多媒体定时器的信息

【讨论】:

    【解决方案5】:

    尝试创建新的System.Threading.Thread 并使用System.Threading.Thread.Sleep。

    var thrd = new Syatem.Threading.Thread(() => {
        while (true) {
            // do something
            System.Threading.Thread.Sleep(1); // wait 1 ms
        }
    });
    
    thrd.Start();
    

    【讨论】:

    • 这并不能保证线程会在 1 毫秒内急剧唤醒
    • 睡眠间隔实际上是12-15ms左右。这是系统时钟提供的最小分辨率。
    • 注意:我有两台 Windows 10 笔记本电脑(i5 和 i7),i7 的精度为 1 毫秒,而 i5(MS Surface)使用 thread.sleep() 的精度为 15.6 毫秒。所以它可能在你的开发机器上工作,它可能不适用于生产。
    • @markgamache 这在 Windows 10 中有所改变; timeBeginPeriod 行为已更改,因此时间可能会因 Win10 版本而异。
    猜你喜欢
    • 1970-01-01
    • 2010-12-02
    • 1970-01-01
    • 2010-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-02
    • 1970-01-01
    相关资源
    最近更新 更多