【问题标题】:High resolution timer [duplicate]高分辨率计时器[重复]
【发布时间】:2011-10-31 12:22:05
【问题描述】:

我想要一个分辨率约为 5 毫秒的计时器。但是.Net中的当前Timer的分辨率约为50ms。 我找不到任何可以创建高分辨率计时器的有效解决方案,尽管有人声称您可以在 C# 中完成。

【问题讨论】:

    标签: c#


    【解决方案1】:

    您可以使用QueryPerformanceCounter()QueryPerformanceTimer(),如this article 中所述。

    【讨论】:

      【解决方案2】:

      对于寻找答案的人来说,迟到仍然可能有用,因为该主题十多年来没有任何改变。

      背景

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

      在不再需要时通过调用 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;
          }
      }
      

      【讨论】:

        【解决方案3】:

        除非频率以毫秒为单位,否则前面的示例不起作用; perf 定时器频率很少以毫秒为单位。

        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 类更精确的计时器。

        【讨论】:

          【解决方案4】:

          这是一个基于秒表定时器的实现
          https://gist.github.com/DraTeots/436019368d32007284f8a12f1ba0f545

          1. 它适用于所有平台,无论StopWatch.IsHighPrecision == true

          2. 它的Elapsed 事件保证不重叠(这可能很重要,因为事件处理程序内部的状态更改可能不受多线程访问的保护)

          这里是如何使用它:

          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 毫秒的结果:

          还值得一提的是:

          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%

          【讨论】:

            【解决方案5】:

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

            它告诉你如何使用多媒体定时器来拥有一个高频定时器。它对我来说很好用!!!

            【讨论】:

            • 如果这确实解决了您的问题,您应该接受它作为正确答案。在这种情况下,完全可以接受您自己的答案,因为此处发布的其他答案似乎都无法解决您的问题。
            • 是的,确实链接坏了。看到这就是为什么我畏缩链接到外部网站的帖子作为答案,并且没有意识到外部网站或网页可能会在这种情况下随时损坏。不用说,Sura 的回答现在对 StackOverflow 社区毫无用处,即使它当时对他或她有用。
            • 是的,但是提到多媒体计时器,您可以想到一个搜索词。从其他网站复制和粘贴代码在某些国家/地区是不合法的,因此链接是合法安全的东西
            • 供参考:svn.the-starport.net/utfeditor/UTFEditor/MultimediaTimer.cs。博客文章似乎对人们产生了一些影响。 :)
            • 这是来自 codeproject 的链接和代码:link 该代码太长了,无法添加。但它使用的是 QueryPerformanceCounter() 和 QueryPerformanceFrequency()。如果链接断开,您可以直接用 Google 搜索。
            【解决方案6】:

            关于 OP 专门询问有关定期触发事件的 Timer 类的信息。我已经修改了这个答案,我的旧答案低于水平线。

            我使用 Timer 类测试了以下代码,它似乎可以在我的机器上至少在 14 - 15 毫秒范围内运行。自己尝试一下,看看是否可以重现。因此,低于 50 毫秒的响应时间是可能的,但不能精确到 1 毫秒。

            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 中使用 Stopwatch 类: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 函数:

            Stopwatch 类协助操作与计时相关的 托管代码中的性能计数器。具体来说,频率 字段和 GetTimestamp 方法可以用来代替非托管 Win32 API QueryPerformanceFrequency 和 QueryPerformanceCounter。

            【讨论】:

            • 据我了解,秒表只能用于准确测量经过的时间。但是我的计时器希望每 5 毫秒触发一次(比如说),并且必须执行一个方法。我看不出我可以使用秒表类来做到这一点。
            • 糟糕。我的印象是你的意思是计时,不一定是驾驶事件。我会四处看看是否有解决此问题的方法。
            • @sura:我刚刚检查了有关间隔属性的 Timer 类的文档:msdn.microsoft.com/en-us/library/…。通过指定间隔 1,它似乎可以降低到至少毫秒的精度。当你设置这个时,它的触发速度是否仍然比平时慢得多?
            • 这是我使用的,但它可以下降到最多 55ms 的间隔,因为计时器间隔不是很精确。另外,我在某处读到,当间隔设置为非常低的水平时,它会引入很多性能开销。无论如何,我找到了一个使用多媒体计时器的解决方案。最后检查我的答案。
            【解决方案7】:

            系统时钟以恒定速率“滴答声”。为了提高 timerdependent function*s 的准确性,调用 **timeGetDevCaps* 来确定支持的最小计时器分辨率。 然后调用 timeBeginPeriod 将计时器分辨率设置为最小值。

            注意:通过调用 timeBeginPeriod,可能会显着影响其他与计时器相关的函数,例如系统时钟、系统电源使用情况和调度程序。因此,开始您的应用程序 使用 timeBeginPeriod 并使用 timeEndPeriod 结束它

            【讨论】:

              【解决方案8】:

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

              【讨论】:

              • +1,因为似乎手头有这么大的课程。 -1 表示不必要的复杂,当 System.Diagnostics 中的 Stopwatch 类完成相同的操作时。我不确定他们是否在 VS 2005 中公开了这一点,但它现在存在,没有理由使用这样的东西。
              • 我不知道秒表有多精确,但性能计数器可以让您进入低微秒范围。我也想知道是什么让它成为一个“大”或“复杂”的类。我应该为此说“-1”吗?
              • 引用文档:The Stopwatch class assists the manipulation of timing-related performance counters within managed code. Specifically, the Frequency field and GetTimestamp method can be used in place of the unmanaged Win32 APIs QueryPerformanceFrequency and QueryPerformanceCounter. 所以没有必要使用这种代码。此外,这是“大”,因为秒表更易于使用。
              • 他想要可行的解决方案,所以我给了他一个。我认为没有理由拒绝投票,因为这是有效的
              • @genesis:这是一个可行的解决方案,但我建议不要使用它,因为Stopwatch 是一种更惯用(更不用说更直接)的方式来完成同样的事情。我不会给这个-1,但我会提醒人们远离这个,而是看向秒表。
              猜你喜欢
              • 1970-01-01
              • 2011-04-02
              • 2010-09-14
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多