忙碌 Loop/Spinning 有时在 Windows 下会花费太长时间

Busy Loop/Spinning sometimes takes too long under Windows

我正在使用 windows 7 PC 以 1kHz 的速率输出电压。起初我只是简单地用 sleep_until(nextStartTime) 结束了线程,但是这已被证明是不可靠的,有时工作正常,有时最多 10 毫秒。

我在这里找到了其他答案,说繁忙的循环可能更准确,但出于某种原因,我的有时也会花费太长时间。

while (true) {
        doStuff();  //is quick enough
        logDelays();

        nextStartTime = chrono::high_resolution_clock::now() + chrono::milliseconds(1);

        spinStart = chrono::high_resolution_clock::now();

        while (chrono::duration_cast<chrono::microseconds>(nextStartTime - 
                         chrono::high_resolution_clock::now()).count() > 200) {
            spinCount++; //a volatile int
        }
        int spintime = chrono::duration_cast<chrono::microseconds>
                              (chrono::high_resolution_clock::now() - spinStart).count();

        cout << "Spin Time micros :" << spintime << endl;

        if (spinCount > 100000000) {
            cout << "reset spincount" << endl;
            spinCount = 0;
        }

}

我希望这能解决我的问题,但它会产生输出:

  Spin Time micros :9999
  Spin Time micros :9999
  ...

过去 5 个小时我一直被这个问题困扰,如果有人知道解决方案,我将不胜感激。

在 Windows 我认为不可能获得如此精确的时间,因为您无法确定 运行您的线程实际上 运行 在您想要的时间。即使使用率较低 CPU 并将线程设置为实时优先级,它仍然可以被中断(据我所知是硬件中断。从未进行过全面调查,但即使是实时的简单 while(true) ++i; 类型循环我也看到过被中断然后在 CPU 个核心之间移动)。虽然实时线程的这种中断和切换非常快,但如果您试图在不缓冲的情况下直接驱动信号,它仍然很重要。

相反,您真正想要读取和写入数字样本的缓冲区(因此在 1KHz 时每个样本为 1ms)。您需要确保在最后一个缓冲区完成之前对另一个缓冲区进行排队,这将限制它们的大小,但是如果代码简单并且没有其他 CPU 争用单个样本缓冲区,则实时优先级为 1KHz( 1ms) 甚至是可能的,这比 "immediate" 多出 1ms 的最坏延迟,但您必须进行测试。然后,您将其留给硬件及其驱动程序来处理精确的计时(例如,确保每个输出样本 "exactly" 1ms 达到供应商声称的精度)。

这基本上意味着您的代码在最坏的情况下只需精确到 1 毫秒,而不是试图追求远小于 OS 真正支持的东西,例如微秒精度。

只要您能够在硬件用完之前的缓冲区之前对新缓冲区进行排队,它就能够 运行 以所需的频率正常运行(再次以音频为例,虽然可容忍的延迟通常要高得多,因此缓冲区也是如此,但如果您使 CPU 过载,您有时仍然会听到应用程序没有及时排队新原始音频的音频故障。

通过仔细计时,您甚至可以通过尽可能长时间地等待处理和排队下一个样本(例如,如果您需要减少输入和输出之间的延迟),将时间缩短到几分之一毫秒,但是请记住,越接近它,提交它的风险就越大。

根据评论,此代码正确等待:

auto start = std::chrono::high_resolution_clock::now();
const auto delay = std::chrono::milliseconds(1);
while (true) {
    doStuff();  //is quick enough
    logDelays();

    auto spinStart = std::chrono::high_resolution_clock::now();
    while (start > std::chrono::high_resolution_clock::now() + delay) {}
    int spintime = std::chrono::duration_cast<std::chrono::microseconds>
                          (std::chrono::high_resolution_clock::now() - spinStart).count();

    std::cout << "Spin Time micros :" << spintime << std::endl;
    start += delay;
}

重要的部分是忙等待 while (start > std::chrono::high_resolution_clock::now() + delay) {}start += delay;,它们结合起来确保等待 delay 的时间量,即使在外部因素(windows更新保持系统繁忙)打扰它。如果循环花费的时间比 delay 长,则循环将被执行而不会等到它赶上(如果 doStuff 足够慢,则可能永远不会)。

请注意,错过一个更新(由于系统繁忙)然后立即发送 2 个来赶上可能不是处理这种情况的最佳方法。如果时间错误超过某个可接受的数量,您可能需要检查 doStuff 和 abort/restart 传输中的当前时间。