高分辨率定时器

18

我想要一个5毫秒左右分辨率的定时器,但是.NET中现有的计时器只有大约50毫秒的分辨率。

虽然有些人声称可以在C#中实现,但我找不到任何可行的解决方案来创建高分辨率定时器。

8个回答

15

关于问题中特别询问的会在正常时间间隔触发事件的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 中的 Stopwatch 类:http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx 您可以查询它以检查它是否具有高分辨率的 IsHighResolution 字段。此外,您还可以检查 Stopwatch 的确切分辨率:
int resolution = 1E9 / Stopwatch.Frequency;
Console.WriteLine("The minimum measurable time on this system is: {0} nanoseconds", resolution);

如果你担心这个实际上是从哪里获取的,文档似乎暗示它实际上内部调用了更低级别的Win32函数:

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


据我所知,秒表只能用于准确测量经过的时间。但我的计时器希望每5毫秒触发一次,并执行一个方法。我不认为我可以使用秒表类来实现这个功能。 - sura
哎呀,我误以为你是指计时,而不是驱动事件。我会找找看是否有解决这个问题的方案。 - Mike Bailey
@sura:我刚查看了 Timer 类的文档,关于间隔属性的部分:http://msdn.microsoft.com/en-us/library/system.timers.timer.interval.aspx。它似乎可以精确到毫秒级别,只需指定一个 1 的间隔即可。当你设置这个值时,它是否仍然比平常慢得多? - Mike Bailey
这是我之前使用的,但它可以降至55毫秒间隔,计时器间隔不太精确。此外,我在某处读到当间隔设置为非常低的水平时,它会引入很多性能开销。无论如何,我找到了一种解决方案,它使用多媒体定时器。请查看我的回答帖子末尾。 - sura

11

这个网址怎么样?

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

5
似乎有这么一个大类在手,给个赞。但是没必要使用这种不必要复杂的方式,因为 System.Diagnostics 中的 Stopwatch 类能够实现同样的功能。我不确定它们是否在 VS 2005 中提供了该类,但现在已经存在了,没有理由使用这种东西。 - Mike Bailey
5
我不知道秒表可以有多精确,但性能计数器可以达到微秒级别。我也想知道这个类为什么被称为“大型”或“复杂”的类。那我应该为此打上“-1”吗? - Razzupaltuff
5
引用文档中的话:“Stopwatch 类帮助在托管代码中操作与计时相关的性能计数器。具体来说,Frequency 字段和 GetTimestamp 方法可以替代非托管 Win32 API QueryPerformanceFrequency 和 QueryPerformanceCounter。” 因此不需要使用这种代码。此外,从 Stopwatch 的角度来看,这是“大型”的,因为它要简单得多。 - Mike Bailey
3
他想要一个可行的方案,所以我给了他一个。我认为没有理由给它负评,因为这个方案是可行的。 - genesis
3
这是一个可行的解决方案,但我建议不要使用它,因为使用Stopwatch是一种更惯用的(更直接)实现相同效果的方法。我不会打负分,但我会告诫人们不要使用这种方法,而是转向使用Stopwatch - Mike Bailey
显示剩余8条评论

7

如果这确实解决了您的问题,您应该将其接受为正确答案。在这种情况下,完全可以接受您自己的答案,因为这里发布的其他答案似乎都不能解决您的问题。 - Mike Bailey
2
是的,链接确实已经失效了。这就是为什么我对那些将外部网站链接作为答案发布并没有意识到外部网站或网页随时可能会出现问题而感到不安的帖子感到不满,就像在这种情况下一样。不用说,尽管Sura的回答当时对他或她有用,但现在对StackOverflow社区来说是无用的。 - ThN
没错,但是如果提到多媒体定时器,你可以考虑使用搜索词。在一些国家,从其他网站复制代码是不合法的,因此提供链接是一个合法的安全选择。 - Offler
参考链接:http://svn.the-starport.net/utfeditor/UTFEditor/MultimediaTimer.cs。这篇博客文章似乎对人们产生了一些影响。 :) - cHao
这里是来自codeproject的链接和代码: 链接 代码太长无法添加。但它使用了QueryPerformanceCounter()和QueryPerformanceFrequency()。 如果链接失效,您可以在Google上搜索它们。 - Jonas
计时器也运行正常。 - Jonas

5

这是基于StopWatch计时器的实现

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毫秒的结果如下: enter image description here 值得一提的是:
  1. 我在Ubuntu上的mono上也获得了相同的精度。
  2. 在进行基准测试时,我看到的最大和非常罕见的偏差约为0.5毫秒(这可能意味着什么,不是实时系统,但仍值得一提)
  3. Stopwatch ticks并不是TimeSpan ticks。 在那台运行Windows 10的机器上,HighResolutionTimer.TickLength为0.23[ns]。
  4. 基准测试的CPU使用率为0.5ms间隔时为10%,200ms间隔时为0.1%。

2

虽然晚了点但还是可能对寻找答案的人有所帮助,因为这个话题已经十年没有变化了。

背景

.NET中的任何延迟操作都会涉及系统时钟分辨率,即使用timeBeginPeriod()设置的分辨率。无论是Thread.Sleep(N)、Threading.Timer还是Waitable.WaitOne(N),都是如此。而DateTime.Now()和System.Diagnostic.Stopwatch的时间分辨率要高得多,因此有一种实现精确计时事件的方法称为hot loop。但是,由于这些循环倾向于占用处理器核心,因此易受操作系统的严重威胁。以下是我们防止这种情况发生的方法:

在热循环中不再需要线程时间量时,通过调用Thread.Sleep(0)或.WaitOne(0)将线程时间量让给其他线程

下面是一个展示高分辨率调度程序简单实现的代码片段:

注:本文中的“hot loop”直译为热循环,但是建议保留原文中的术语以便更好地理解。

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

我们如何将其用作IScheduler的常规实现?我想要这个用于System.Reactive。我需要在Observable.Interval中产生大约60 Hz的tick。 - SuperJMN
1
不知道,伙计。试着发挥创造力,这个东西可以以任何速率产生 ticks,你需要驾驭它们并发送到任何地方。 - Vitaly

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

此函数以秒为单位返回性能计数器。您使用性能计时器的原因是与传统的C++代码共享计时器,或获得比C# StopWatch类更精确的计时器。


0
系统时钟以恒定速率“滴答”运行。为了提高计时器相关函数的准确性,请调用*timeGetDevCaps*来确定支持的最小计时器分辨率。然后调用*timeBeginPeriod*将计时器分辨率设置为其最小值。
注意:通过调用*timeBeginPeriod*,其他计时器相关函数可能会受到显著影响,例如系统时钟、系统功耗和调度程序。因此,请在应用程序启动时使用*timeBeginPeriod*,并在结束时使用*timeEndPeriod*。

0

您可以使用本文中概述的QueryPerformanceCounter()QueryPerformanceTimer()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接