【发布时间】:2011-10-31 12:22:05
【问题描述】:
我想要一个分辨率约为 5 毫秒的计时器。但是.Net中的当前Timer的分辨率约为50ms。 我找不到任何可以创建高分辨率计时器的有效解决方案,尽管有人声称您可以在 C# 中完成。
【问题讨论】:
标签: c#
我想要一个分辨率约为 5 毫秒的计时器。但是.Net中的当前Timer的分辨率约为50ms。 我找不到任何可以创建高分辨率计时器的有效解决方案,尽管有人声称您可以在 C# 中完成。
【问题讨论】:
标签: c#
您可以使用QueryPerformanceCounter() 和QueryPerformanceTimer(),如this article 中所述。
【讨论】:
对于寻找答案的人来说,迟到仍然可能有用,因为该主题十多年来没有任何改变。
任何 .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;
}
}
【讨论】:
除非频率以毫秒为单位,否则前面的示例不起作用; 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 类更精确的计时器。
【讨论】:
这是一个基于秒表定时器的实现
https://gist.github.com/DraTeots/436019368d32007284f8a12f1ba0f545
它适用于所有平台,无论StopWatch.IsHighPrecision == true
它的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 毫秒的结果:
还值得一提的是:
我在 Ubuntu 上的单声道精度相同。
在使用基准测试时,我看到的最大和非常罕见的偏差约为 0.5 毫秒 (这可能没有任何意义,它不是实时系统,但仍然值得一提)
Stopwatch ticks are not TimeSpan ticks. 在该 Windows 10 机器上 HighResolutionTimer.TickLength 为 0.23[ns]。
CPU 使用率基准测试为 0.5ms 间隔为 10%,200ms 间隔为 0.1%
【讨论】:
我在以下博客中找到了解决此问题的方法: http://web.archive.org/web/20110910100053/http://www.indigo79.net/archives/27#comment-255
它告诉你如何使用多媒体定时器来拥有一个高频定时器。它对我来说很好用!!!
【讨论】:
关于 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。
【讨论】:
Timer 类的文档:msdn.microsoft.com/en-us/library/…。通过指定间隔 1,它似乎可以降低到至少毫秒的精度。当你设置这个时,它的触发速度是否仍然比平时慢得多?
系统时钟以恒定速率“滴答声”。为了提高 timerdependent function*s 的准确性,调用 **timeGetDevCaps* 来确定支持的最小计时器分辨率。 然后调用 timeBeginPeriod 将计时器分辨率设置为最小值。
注意:通过调用 timeBeginPeriod,可能会显着影响其他与计时器相关的函数,例如系统时钟、系统电源使用情况和调度程序。因此,开始您的应用程序 使用 timeBeginPeriod 并使用 timeEndPeriod 结束它
【讨论】:
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);
}
}
【讨论】:
System.Diagnostics 中的 Stopwatch 类完成相同的操作时。我不确定他们是否在 VS 2005 中公开了这一点,但它现在存在,没有理由使用这样的东西。
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. 所以没有必要使用这种代码。此外,这是“大”,因为秒表更易于使用。
Stopwatch 是一种更惯用(更不用说更直接)的方式来完成同样的事情。我不会给这个-1,但我会提醒人们远离这个,而是看向秒表。