9

我正在寻找创建一个高频回调线程。本质上,我需要一个以常规高频(高达 100Hz)间隔执行的函数。我意识到windows有一个正常的线程执行切片是~15ms。我想指定一个可以快于 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 停止计时器回调。这有点灵活,因为您也可以创建自定义队列。但是,如果只需要一个实例,最简单的实现是使用默认队列。

我使用带有旋转环的秒表测试了这个解决方案,我收到的关于计时的结果几乎相同。但是,我的机器上的 CPU 负载明显不同。

带自旋循环的秒表 - 约 12-15% 的恒定 CPU 负载(大约是我的一个内核的 50%) CreateTimerQueueTimer - 约 3-4% 的恒定 CPU 负载

我还觉得使用 CreateTimerQueueTimer 选项可以减少代码维护。因为它不需要将逻辑添加到您的代码流中。

4

4 回答 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 毫秒的速率并休眠 10 秒才能使其真正安全。不要在电池供电的机器上运行它。支持 timeSetEvent()。

于 2013-05-19T08:48:11.070 回答
1

您可能想尝试StopWatch,它使用与 DirectX 相同的高性能计时器。

于 2013-05-19T00:26:42.523 回答
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 毫秒的间隔!我想如果有更多的争用可能会很快上升,尽管我确实打开了许多应用程序,但没有任何磨合。

在睡眠 10 毫秒(即 100 赫兹)时,平均为 0.0109 ......几乎是正确的!

于 2014-02-20T07:16:16.953 回答
0

请注意,15ms 是有效最短时间的部分原因Sleep是它可能需要这个数量级来实际换出您的进程,换入另一个进程,让它有时间做任何事情,换出并换入您的进程再次。假设您有多个内核,将一个专用于处理您的 100Hz 更新程序可能是最好的,特别是如果您知道何时希望跳过几个周期进行手动生成的垃圾收集,和/或按照建议使用 C/C++。

于 2013-05-19T06:53:37.413 回答