高频计时 .NET

9
我想创建一个高频回调线程。基本上我需要一个函数以正常的高频(高达100Hz)间隔执行。我知道Windows的普通线程执行片段大约是15毫秒。我希望指定一个可以比15毫秒更快的常规间隔。
这就是我要完成的任务。我有一个外部设备,需要在特定间隔内收到消息。间隔取决于情况而变化。我预计我不会需要超过100Hz(10ms)的消息速率。
当然,我可以实现自旋循环,但我希望有一种解决方案,不需要浪费太多资源。
提供的问题/答案链接并未解决此问题。虽然我同意这个问题已经以几种不同的方式被问过了,但还没有一个好的解决方案来解决这个问题。
提供的大多数答案都涉及使用Stopwatch并手动执行定时任务,这完全太消耗CPU了。唯一可行的解决方案是使用多媒体定时器,但像Haans所提到的那样存在一些缺陷。我已经找到另一个解决方案,在下面添加。我目前不知道其中的缺陷,但我打算进行一些测试和研究。我仍然对这个解决方案的评论感兴趣。
WINAPI调用方式:
BOOL WINAPI CreateTimerQueueTimer(
  _Out_     PHANDLE phNewTimer,
  _In_opt_  HANDLE TimerQueue,
  _In_      WAITORTIMERCALLBACK Callback,
  _In_opt_  PVOID Parameter,
  _In_      DWORD DueTime,
  _In_      DWORD Period,
  _In_      ULONG Flags
);

并且

BOOL WINAPI DeleteTimerQueueTimer(
  _In_opt_  HANDLE TimerQueue,
  _In_      HANDLE Timer,
  _In_opt_  HANDLE CompletionEvent
);

链接 - http://msdn.microsoft.com/en-us/library/windows/desktop/ms682485%28v=vs.85%29.aspx

我正在使用PInvoke来完成这项工作。当处理多媒体定时器时,也需要使用它。

对于那些感兴趣的人,我的PInvoke签名如下。 Pinvoke链接

[DllImport("kernel32.dll")]
static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer,
   IntPtr TimerQueue, WaitOrTimerDelegate Callback, IntPtr Parameter,
   uint DueTime, uint Period, uint Flags);

// This is the callback delegate to use.
public delegate void WaitOrTimerDelegate (IntPtr lpParameter, bool TimerOrWaitFired);

[DllImport("kernel32.dll")]
static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer,
   IntPtr CompletionEvent);

使用CreateTimerQueueTimer启动计时器回调。使用DeleteTimerQueueTimer停止计时器回调。您还可以创建自定义队列,使其更具灵活性。然而,如果只需要单个实例,则最简单的实现方法是使用默认队列。
我测试了这个解决方案和一个使用带有自旋循环的Stopwatch的解决方案,结果表明它们在时间上几乎相同。然而,在我的机器上CPU负载显著不同。
使用自旋循环的Stopwatch- ~12-15%的恒定CPU负载(大约占用了一个核心的50%)
CreateTimerQueueTimer- ~3-4%的恒定CPU负载
我也觉得使用CreateTimerQueueTimer选项将减少代码维护,因为它不需要添加逻辑到您的代码流程中。
4个回答

11

有很多不准确的信息通过链接和评论进行传播。默认情况下,在大多数计算机上,时钟滴答中断为1/64秒=15.625毫秒,但可以更改。这也是一些计算机似乎以其他速率运行的原因。可从winmm.dll中获得的Windows多媒体API允许您进行调整。

应该使用秒表来获取准确的时间间隔测量,除非你在一个热循环中使用它,该循环不断检查是否已经过去了指定的时间。当线程的时间片到期时,Windows对这样的线程并不友好,如果其他线程竞争处理器,该线程将在一段时间内无法被重新安排运行。由于您通常不会在其他进程正在活动地运行和消耗CPU时间时调试代码,这种效果很容易被忽视。

您需要使用的函数是timeSetEvent(),它提供了一种高度精确的计时器,最低可以达到1毫秒。它是自我校正的,如果必要(并且可能),会缩短时间间隔以补偿前一个回调因调度限制而延迟的时间。但请注意,它很难使用,回调是从线程池线程中进行的,类似于System.Threading.Timer,因此请确保使用安全的交互锁定并采取措施以确保您不会因重入而遇到问题。

完全不同的方法是timeBeginPeriod(),它改变时钟中断率。这会产生许多副作用之一是Thread.Sleep()更加准确。这往往是更容易的解决方案,因为您可以使其同步。只需睡眠1毫秒即可获得时钟中断率。以下是一些演示其工作方式的代码:

using System;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Threading;

class Program {
    static void Main(string[] args) {
        timeBeginPeriod(10);
        while (!Console.KeyAvailable) {
            var sw = Stopwatch.StartNew();
            for (int ix = 0; ix < 100; ++ix) Thread.Sleep(1);
            sw.Stop();
            Console.WriteLine("{0} msec", sw.ElapsedMilliseconds);
        }
        timeEndPeriod(10);
    }

    [DllImport("winmm.dll")]
    public static extern uint timeBeginPeriod(int msec);
    [DllImport("winmm.dll")]
    public static extern uint timeEndPeriod(int msec);
}

我的机器上的输出:

1001 msec
995 msec
999 msec
999 msec
999 msec
991 msec
999 msec
999 msec
999 msec
999 msec
999 msec
990 msec
999 msec
998 msec
...

但是请注意,这种方法存在一个问题,如果您的计算机上的另一个进程已将时钟中断率降低至10毫秒以下,则此代码不会超过1秒。这很麻烦,您只能通过请求1毫秒速率并休眠10毫秒来确保安全。不要在电池供电的计算机上运行该代码。使用timeSetEvent()更好。


1
谢谢Hans,我一直很欣赏你的回复和建议。实际上,在研究你提到的API时,我找到了解决我的问题的东西。那就是使用CreateTimerQueueTimer。这似乎为我提供了与你提到的API相同的分辨率,但没有关于更改定时事件的竞争的缺点。另一个附加的好处是它来自kernal32.dll。对于CreateTimerQueueTimer API类有什么想法吗? - galford13x
是的,它是timeSetEvent()的推荐替代品。我不太清楚它改变时钟中断率的能力,你需要自己尝试一下。 - Hans Passant
由于无法提供更多答案,我将其标记为答案。它提供了一个适用于我的情况的解决方案。 - galford13x

1

您可能想尝试秒表,它使用了DirectX所使用的高性能计时器。


1
请在可用时检查IsHighResolution - Sani Huttunen
1
我认为这在大多数现代硬件上应该是正确的。 - Ali B
1
我不确定这如何有所帮助。我需要在小于15毫秒的时间间隔内发生特定的回调。虽然秒表可以给我精确的计时,但是为了达到目标,我必须在循环中不断检查时间,从而烧掉CPU,这正是我试图避免的(烧掉CPU)。 - galford13x
2
@galford13x:虽然StopWatch没有内置回调函数,但您仍然可以尝试在循环中检查它,您可能能够获得亚毫秒级的计时。如果您需要,我可以提供一些代码。无论如何,使用非确定性的GC语言(如C#)来进行近实时编程并不是最佳选择。更好的方法是使用C/C++编写此部分代码。 - Ali B
1
@Ali B:我已经有了你提到的使用自旋循环的解决方案。但是,正如我之前所说的那样,它需要大量的CPU资源。将这部分代码用C/C++编写会有什么帮助呢?我可以使用无人对象在C#中编写代码,以停止GC对该部分或通过C#访问的任何对象所在线程的影响。我不需要严格的实时操作。如果一个执行晚了2毫秒,但下一个执行早了2毫秒,那么我的目标就达成了。 - galford13x
显示剩余3条评论

0

Thread.Sleep() 看起来在如此小的时间间隔内并不像你想象的那样不准确,在 Windows 8.1 x64 i7 笔记本电脑上 .NET 4.0 上运行如下:

using System;
using System.Diagnostics;
using System.Threading;

namespace HighFrequency
{
    class Program
    {
        static void Main(string[] args)
        {
            var count = 0;
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            while(count <= 1000)
            {
                Thread.Sleep(1);
                count++;
            }
            stopwatch.Stop();
            Console.WriteLine("C# .NET 4.0 avg. {0}", stopwatch.Elapsed.TotalSeconds / count);
        }
    }
}

输出:

C# .NET 4.0 平均值为 0.00197391928071928

所以只有不到2毫秒的间隔!我猜如果有更多争用,这个时间可能会很快上升,尽管我打开了许多应用程序,但没有什么在运行。

在休眠10ms即100HZ时,平均值为0.0109...几乎完美!


除非Win8的睡眠调用有所改变,否则一般来说,睡眠不是保持时间的好机制。此外,当系统上的高频计时器发生变化时,睡眠的最小时间可能是不可预测的。总之,在您获得约10毫秒的时间时,当我在我的Win7机器上运行此代码时,平均值为15.6毫秒。此外,我不是在寻找平均值,而是在寻找精确一致的时间,因此12毫秒、9毫秒、11毫秒、8毫秒都是不可接受的。 - galford13x
没错,我不会用它来计时,只是用于高频循环 - 我会使用 Stopwatch 来测量实际经过的时间 - 但如果您想要一致的时间,这仍然不是一个选项。 - markmnl

0
请注意,15毫秒是有效的最小Sleep时间的部分原因是,实际上需要这个数量级才能交换您的进程,将另一个进程交换进来,让它有一些时间来执行任何操作,然后将其交换出去并再次将您的进程交换回来。假设您有多个核心,将其中一个专门用于处理100Hz更新程序可能是最好的选择,特别是如果您知道何时要跳过几个周期进行手动生成的垃圾收集和/或使用建议的C/C++。

更不用说交换上下文发生的最小频率是不同于 Windows 客户端和服务器版本的。请参阅 Windows 量子。 - Peter Ritchie
15毫秒起作用的原因就像Haans所说的那样,这是Windows给线程分配的最小时间片。当然,正如他所说,这取决于平台和操作系统版本。您可以编写一个多线程应用程序来记录时间并使用Thread.Sleep(2)几秒钟,然后打印出线程中记录的所有时间。您会注意到它们相隔约15.6毫秒。当然,线程可以通过调用Thread.Sleep(0)放弃其余的时间片给更重要的任务而使用少于15毫秒的时间。 - galford13x

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