C#中的高分辨率计时器

时间:2014-07-19 10:02:08

标签: c# timer high-resolution

是否有高分辨率计时器在每次计时器过去时引发事件,就像System.Timer类一样?我需要每分钟Elapse的高分辨率计时器。

我一直在发帖说明秒表可以测量高分辨率,但我不想测量时间,我想创建一个1毫秒的间隔。

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

6 个答案:

答案 0 :(得分:11)

我所知道的.NET框架中没有任何内置功能。 Windows具有通过Multimedia Timer API进行高分辨率计时器事件的机制。下面是一个快速的例子我掀起了似乎做这个工作。似乎还有一个很好的例子here

我会注意到此API会更改可能降低系统性能的系统范围设置,因此买家要小心。出于测试目的,我建议跟踪定时器触发的频率,以验证定时与您尝试模拟的设备类似。由于Windows不是实时操作系统,因此系统上的负载可能导致MM定时器延迟,导致100 ms的间隙快速连续包含100个事件,而不是间隔1 ms的100个事件。关于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);
}

答案 1 :(得分:2)

你可以拥有一个基于秒表的高分辨率计时器,它可以提供比现今系统更好的1ms分辨率。

以下是我在Stackoverflow上的另一个答案https://stackoverflow.com/a/45097518/548894

实施https://gist.github.com/DraTeots/436019368d32007284f8a12f1ba0f545

  1. 它适用于所有平台,并且只要StopWatch.IsHighPrecision == true

  2. 就具有高精度
  3. 它的Elapsed事件保证不重叠(这可能很重要,因为事件处理程序中的状态更改可能不受多线程访问保护)

  4. 以下是如何使用它:

    Console.WriteLine($"IsHighResolution = {HighResolutionTimer.IsHighResolution}");
    Console.WriteLine($"Tick time length = {HighResolutionTimer.TickLength} [ms]");
    
    var timer = new HighResolutionTimer(0.5f);
    
    // UseHighPriorityThread = true, sets the execution thread 
    // to ThreadPriority.Highest.  It doesn't provide any precision gain
    // in most of the cases and may do things worse for other threads. 
    // It is suggested to do some studies before leaving it true
    timer.UseHighPriorityThread = false;
    
    timer.Elapsed += (s, e) => { /*... e.Delay*/ }; // The call back with real delay info
    timer.Start();  
    timer.Stop();    // by default Stop waits for thread.Join()
                     // which, if called not from Elapsed subscribers,
                     // would mean that all Elapsed subscribers
                     // are finished when the Stop function exits 
    timer.Stop(joinThread:false)   // Use if you don't care and don't want to wait
    

    这是一个基准(和实例):
    https://gist.github.com/DraTeots/5f454968ae84122b526651ad2d6ef2a3

    在Windows 10上将计时器设置为0.5毫秒的结果: enter image description here

    值得一提的是:

    1. 我在Ubuntu上的单声道精度相同。

    2. 在玩基准时,我看到的最大和非常罕见的偏差约为0.5毫秒 (这可能意味着什么,它不是实时系统,但仍然值得一提)

    3. Stopwatch ticks are not TimeSpan ticks. 在该Windows 10计算机上 HighResolutionTimer.TickLength为0.23 [ns]。

答案 2 :(得分:1)

恐怕是陈旧的答案。 .net 中高分辨率计时的最终解决方案是 System.Diagnostics.Stopwatch 类。如果运行代码的系统具有高分辨率计时器硬件,则此类使用高分辨率计时器(有时精度为纳秒级)。如果不是,则返回到标准的 Windows 计时器,其精度约为 50 毫秒。

在过去十年中,几乎每台机器都配备了高分辨率计时器。

如果由于某种可怕的不幸,您不得不在极其陈旧的硬件上运行,那么上面给出的多媒体计时器解决方案可以提供毫秒级的精度(但会降低整体系统性能)。

值得注意的是,这个问题已有 6 年历史了,因此原始海报完全有可能在过时的硬件上运行。只需使用 Stopwatch

答案 3 :(得分:1)

我无法让 Mike 的解决方案发挥作用,并基于此代码项目文章 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;

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

    public uint StartElapsedTimer()
    {
        var myData = 1; //dummy data
        return timeSetEvent(_elapsedMs, _resolutionMs / 10, new TimerEventHandler(TickHandler), 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();
    }
}

输出看起来像这样

enter image description here

答案 4 :(得分:0)

尝试创建新的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();

答案 5 :(得分:0)

有一个选项:使用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