高分辨率计时器

时间:2011-08-21 08:02:01

标签: c#

我想要一个大约5毫秒分辨率的计时器。但.Net中的当前Timer具有大约50ms的分辨率。 我找不到任何可以创建高分辨率计时器的工作解决方案,尽管有些人声称你可以在C#中做到这一点。

8 个答案:

答案 0 :(得分:15)

关于OP特别询问有关定期触发事件的Timer类的信息。我修改了这个答案,我的旧答案低于横向规则。

我使用Timer类测试了以下代码,看起来它至少可以在我的机器上达到14-15毫秒的范围内。亲自尝试一下,看看你是否可以重现这一点。所以可能有50毫秒以下的响应时间,但它不能精确到一毫秒。

using System;
using System.Timers;
using System.Diagnostics;

public static class Test
{
    public static void Main(String[] args)
    {
        Timer timer = new Timer();
        timer.Interval = 1;
        timer.Enabled = true;

        Stopwatch sw = Stopwatch.StartNew();
        long start = 0;
        long end = sw.ElapsedMilliseconds;

        timer.Elapsed += (o, e) =>
        {
            start = end;
            end = sw.ElapsedMilliseconds;
            Console.WriteLine("{0} milliseconds passed", end - start);
        };

        Console.ReadLine();
    }
}

注意:以下是我的老答案,当时我认为OP正在讨论时间问题。以下仅仅是关于事物持续时间的定时的有用信息,但是没有提供以规则间隔触发事件的任何方式。为此,Timer类是必要的。

尝试使用System.Diagnostics中的秒表课程:http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx

您可以通过它的IsHighResolution字段查询它以检查它是否具有高分辨率。此外,您可以检查秒表的确切分辨率:

int resolution = 1E9 / Stopwatch.Frequency;
Console.WriteLine("The minimum measurable time on this system is: {0} nanoseconds", resolution);

如果您担心实际来源的来源,文档似乎暗示它实际上在内部调用较低级别的Win32函数:

  

秒表课程有助于操纵与时间相关的操作   托管代码中的性能计数器。具体来说,频率   可以使用字段和GetTimestamp方法代替非托管方法   Win32 API QueryPerformanceFrequency和QueryPerformanceCounter。

答案 1 :(得分:11)

this一个怎么样?

public class HiResTimer
{
    private bool isPerfCounterSupported = false;
    private Int64 frequency = 0;

    // Windows CE native library with QueryPerformanceCounter().
    private const string lib = "coredll.dll";
    [DllImport(lib)]
    private static extern int QueryPerformanceCounter(ref Int64 count);
    [DllImport(lib)]
    private static extern int QueryPerformanceFrequency(ref Int64 frequency);

    public HiResTimer()
    {
        // Query the high-resolution timer only if it is supported.
        // A returned frequency of 1000 typically indicates that it is not
        // supported and is emulated by the OS using the same value that is
        // returned by Environment.TickCount.
        // A return value of 0 indicates that the performance counter is
        // not supported.
        int returnVal = QueryPerformanceFrequency(ref frequency);

        if (returnVal != 0 && frequency != 1000)
        {
            // The performance counter is supported.
            isPerfCounterSupported = true;
        }
        else
        {
            // The performance counter is not supported. Use
            // Environment.TickCount instead.
            frequency = 1000;
        }
    }

    public Int64 Frequency
    {
        get
        {
            return frequency;
        }
    }

    public Int64 Value
    {
        get
        {
            Int64 tickCount = 0;

            if (isPerfCounterSupported)
            {
                // Get the value here if the counter is supported.
                QueryPerformanceCounter(ref tickCount);
                return tickCount;
            }
            else
            {
                // Otherwise, use Environment.TickCount.
                return (Int64)Environment.TickCount;
            }
        }
    }

    static void Main()
    {
        HiResTimer timer = new HiResTimer();

        // This example shows how to use the high-resolution counter to 
        // time an operation. 

        // Get counter value before the operation starts.
        Int64 counterAtStart = timer.Value;

        // Perform an operation that takes a measureable amount of time.
        for (int count = 0; count < 10000; count++)
        {
            count++;
            count--;
        }

        // Get counter value when the operation ends.
        Int64 counterAtEnd = timer.Value;

        // Get time elapsed in tenths of a millisecond.
        Int64 timeElapsedInTicks = counterAtEnd - counterAtStart;
        Int64 timeElapseInTenthsOfMilliseconds =
            (timeElapsedInTicks * 10000) / timer.Frequency;

        MessageBox.Show("Time Spent in operation (tenths of ms) "
                       + timeElapseInTenthsOfMilliseconds +
                       "\nCounter Value At Start: " + counterAtStart +
                       "\nCounter Value At End : " + counterAtEnd +
                       "\nCounter Frequency : " + timer.Frequency);
    }
}

答案 2 :(得分:8)

我在以下博客中找到了解决此问题的方法: http://web.archive.org/web/20110910100053/http://www.indigo79.net/archives/27#comment-255

它告诉您如何使用多媒体计时器来设置高频定时器。对我来说工作得很好!!!

答案 3 :(得分:3)

这是一个基于StopWatch计时器的实现 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]。

    4. 基准的CPU使用率为0.5ms间隔为10%,间隔为200ms为0.1%

答案 4 :(得分:0)

系统时钟“打勾”以恒定速率。为了提高定时器相关功能* 的准确性,请调用* * timeGetDevCaps * ,这决定了支持的最小定时器分辨率。 然后调用 timeBeginPeriod 将计时器分辨率设置为最小值。

注意:通过调用timeBeginPeriod,可能会严重影响其他与计时器相关的功能,例如系统时钟,系统电源使用情况和调度程序。因此使用 timeBeginPeriod 启动您的应用,并以 timeEndPeriod

结束

答案 5 :(得分:0)

您可以使用this article中概述的QueryPerformanceCounter()和QueryPerformanceTimer()。

答案 6 :(得分:0)

除非频率以毫秒为单位,否则上一个示例不起作用。性能定时器的频率很少以毫秒为单位。

private static Int64 m_iPerfFrequency = -1;

public static double GetPerfCounter()
{
    // see if we need to get the frequency
    if (m_iPerfFrequency < 0)
    {
        if (QueryPerformanceFrequency(out m_iPerfFrequency) == 0)
        {
            return 0.0;
        }
    }

    Int64 iCount = 0;
    if (QueryPerformanceCounter(out iCount) == 0)
    {
        return 0.0;
    }

    return (double)iCount / (double)m_iPerfFrequency;
}

[DllImport("kernel32.dll", SetLastError = true)]
public static extern int QueryPerformanceCounter(out Int64 iCount);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern int QueryPerformanceFrequency(out Int64 iFrequency);

这将以秒为单位返回性能计数器。使用perf计时器的原因是与旧版C ++代码共享计时器,或者获得比C#StopWatch类更精确的计时器。

答案 7 :(得分:0)

参加聚会很晚,对于寻找答案的人可能仍然有用,因为十年来该主题没有任何变化。

背景

任何.NET延迟指令总是归结为系统时钟分辨率,即您使用timeBeginPeriod()设置的分辨率。无论是Thread.Sleep(N),Threading.Timer还是Waitable.WaitOne(N)。但是DateTime.Now()和System.Diagnostic.Stopwatch的时间分辨率更高,因此有一种方法可以实现称为 hot loop 的精确计时事件。热循环却容易受到操作系统的严重威胁,因为热循环往往会完全占用处理器核心。这是我们为防止这种情况所做的工作:

一旦不再需要,可以通过调用Thread.Sleep(0)或.WaitOne(0) 将热线程中的线程线程时间放弃给其他线程

下面的代码片段显示了高分辨率调度程序的简单实现:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// High resolution scheduler. 
/// License: public domain (no restrictions or obligations)
/// Author: Vitaly Vinogradov
/// </summary>
public class HiResScheduler : IDisposable
{
    /// <summary>
    /// Scheduler would automatically downgrade itself to cold loop (Sleep(1)) when there are no
    /// tasks earlier than the treshold. 
    /// </summary>
    public const int HOT_LOOP_TRESHOLD_MS = 16;

    protected class Subscriber : IComparable<Subscriber>, IComparable
    {
        public Action Callback { get; set; }
        public double DelayMs { get; set; }

        public Subscriber(double delay, Action callback)
        {
            DelayMs = delay;
            Callback = callback;
        }

        public int CompareTo(Subscriber other)
        {
            return DelayMs.CompareTo(other.DelayMs);
        }

        public int CompareTo(object obj)
        {
            if (ReferenceEquals(obj, null))
                return -1;
            var other = obj as Subscriber;
            if (ReferenceEquals(other, null))
                return -1;
            return CompareTo(other);
        }
    }

    private Thread _spinner;
    private ManualResetEvent _allowed = new ManualResetEvent(false);
    private AutoResetEvent _wakeFromColdLoop = new AutoResetEvent(false);
    private bool _disposing = false;
    private bool _adding = false;

    private List<Subscriber> _subscribers = new List<Subscriber>();
    private List<Subscriber> _pendingSubscribers = new List<Subscriber>();

    public bool IsActive { get { return _allowed.WaitOne(0); } }

    public HiResScheduler()
    {
        _spinner = new Thread(DoSpin);
        _spinner.Start();
    }

    public void Start()
    {
        _allowed.Set();
    }

    public void Pause()
    {
        _allowed.Reset();
    }

    public void Enqueue(double delayMs, Action callback)
    {
        lock (_pendingSubscribers)
        {
            _pendingSubscribers.Add(new Subscriber(delayMs, callback));
            _adding = true;
            if (delayMs <= HOT_LOOP_TRESHOLD_MS * 2)
                _wakeFromColdLoop.Set();
        }
    }

    private void DoSpin(object obj)
    {
        var sw = new Stopwatch();
        sw.Start();
        var nextFire = null as Subscriber;
        while (!_disposing)
        {
            _allowed.WaitOne();
            if (nextFire != null && sw.Elapsed.TotalMilliseconds >= nextFire?.DelayMs)
            {
                var diff = sw.Elapsed.TotalMilliseconds;
                sw.Restart();

                foreach (var item in _subscribers)
                    item.DelayMs -= diff;

                foreach (var item in _subscribers.Where(p => p.DelayMs <= 0).ToList())
                {
                    item.Callback?.Invoke();
                    _subscribers.Remove(item);
                }
                nextFire = _subscribers.FirstOrDefault();
            }

            if (_adding)
                lock (_pendingSubscribers)
                {
                    _subscribers.AddRange(_pendingSubscribers);
                    _pendingSubscribers.Clear();
                    _subscribers.Sort();
                    _adding = false;
                    nextFire = _subscribers.FirstOrDefault();
                }

            if (nextFire == null || nextFire.DelayMs > HOT_LOOP_TRESHOLD_MS)
                _wakeFromColdLoop.WaitOne(1);
            else
                _wakeFromColdLoop.WaitOne(0);
        }
    }

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