Windows(也包括OS X、Linux)上Qt中的高分辨率定期计时器

High resolution periodic timer in Qt on Windows (also OS X, Linux)

本文关键字:Qt 高分辨率 计时器 包括 OS Linux Windows      更新时间:2023-10-16

到目前为止,我所发现的关于计时器的一切都是,它充其量只能以1ms的分辨率使用。QTimer的文档声称这是它能提供的最好的。

我知道像Windows这样的操作系统不是实时操作系统,但我仍然想问这个问题,希望有人知道一些可以帮助我的东西。

因此,我正在编写一个应用程序,它要求以相当精确但任意的间隔调用一个函数,比如60次/秒(全范围:59-61Hz(。这意味着我需要平均每16.67ms调用一次。这部分设计不会改变。

我目前拥有的最好的定时源是vsync。当我离开的时候,它很好。这并不理想,因为监视器的频率并不完全是我需要调用此函数的频率,但它可以得到一定的补偿。

关键是,给定我所追求的范围,计时器或多或少可以提供精度水平,但不是我想要的精度水平。我可以让一个16毫秒的计时器准确地达到16毫秒~97%的时间。我可以让一个17毫秒的计时器准确地达到17毫秒~97%的时间。但没有API可以让我得到16.67?

我想要的是不是根本不可能?

背景:该项目名为"凤凰"。从本质上讲,它是一个libretro前端。Libretro"核心"是封装在单独共享库中的游戏机模拟器。以特定速率调用的API函数是retro_run()。每个调用都模拟一个游戏帧,并调用音频、视频等的回调。为了以控制台的本地帧速率进行模拟,我们必须以完全(或接近(这个速率调用retro_run((,从而调用计时器。

您可以编写一个循环来检查std::chrono::high_resolution_clock()std::this_thread::yield(),直到经过正确的时间。如果在进行此操作时程序需要响应,则应该在独立于检查主循环的线程中执行。

一些示例代码:http://en.cppreference.com/w/cpp/thread/yield

另一种选择是使用值为PerformanceCounterQElapsedTimer。您仍然需要从循环中检查它,并且可能仍然希望在该循环中屈服。示例代码:http://doc.qt.io/qt-4.8/qelapsedtimer.html

完全没有必要在任何高度受控的时间调用retro_run,特别是只要平均帧速率正确,并且音频输出缓冲区不下溢。

首先,您可能需要使用基于音频输出的计时器来测量实时性。最终,每个retro_run产生一个音频块。添加了区块的音频缓冲区状态是您的时间参考:如果运行得早,缓冲区将太满,如果运行得晚,缓冲区会太空。

这个错误度量可以输入到PI控制器中,该控制器的输出为您提供所需的延迟,直到下一次调用retro_run。这将自动确保您的平均速率阶段是正确的。使retro_run激活的任何系统延迟都将被积分掉,等等。

其次,你需要一种在正确的时间唤醒自己的方法。给定调用retro_run的目标时间(例如性能计数器(,您将需要唤醒代码的事件源,以便在必要时比较时间和retro_run

最简单的方法是重新实现QCoreApplication::notify。在交付每个事件、每个事件循环、每个线程之前,您将有机会使用retro_run。由于系统事件可能不会经常发生,因此您还需要运行计时器来提供更可靠的事件源。事件是什么并不重要:任何类型的事件都有利于你的目的。

我不熟悉retro_run的线程限制——也许您可以一次在任何一个线程中运行它。在这种情况下,您可能希望在池中的下一个可用线程上运行它,但主线程除外。因此,有效地,事件(包括定时器事件(被用作提供执行上下文的能量廉价的来源。

如果您选择有一个专用于retro_run的线程,那么它应该是一个高优先级线程,只需阻塞互斥锁。每当你准备好在一个适时的事件到来时运行retro_run时,你就会解锁互斥锁,并且应该立即调度线程,因为它会抢占大多数其他线程,当然也会抢占进程中的所有线程。

OTOH,在低核心计数系统上,高优先级线程可能会抢占主(gui(线程,因此您还可以直接从获得适时事件的任何线程调用retro_run

当然,使用来自任意线程的事件来唤醒专用线程可能会引入太多最坏情况下的延迟或太多延迟扩散——这将是特定于系统的,您可能希望实时收集运行时统计信息、切换线程和事件源策略,并坚持使用最佳策略。选择是:

  1. retro_run在等待互斥的专用线程中,解锁源是任何通过notify、捕获到定时事件的线程

  2. 在专用线程中等待定时器(或任何其他(事件的CCD_ 20;仍然通过notify、捕获的事件

  3. gui线程中的retro_run,解锁源是传递到gui线程的事件,仍然通过notify、捕获

  4. 以上任何一个,但只使用计时器事件-注意,你不在乎它们是哪个计时器事件,它们不需要来自你的计时器

  5. 如#4中所述,但仅对计时器有选择性。

我的实现基于Lorehead的答案。所有变量的时间均以毫秒为单位。当然,它需要一种停止运行的方法,我也在考虑减去timeElapsedinterval之间的一半(运行平均值(差,使平均值+-n而不是+2n,其中2n是平均过冲。

// Typical interval value: 1/60s ~= 16.67ms
void Looper::beginLoop( double interval ) {
    QElapsedTimer timer;
    int counter = 1;
    int printEvery = 240;
    int yieldCounter = 0;
    double timeElapsed = 0.0;
    forever {
        if( timeElapsed > interval ) {
            timer.start();
            counter++;
            if( counter % printEvery == 0 )  {
                qDebug() << "Yield() ran" << yieldCounter << "times";
                qDebug() << "timeElapsed =" << timeElapsed << "ms | interval =" << interval << "ms";
                qDebug() << "Difference:" << timeElapsed - interval << " -- " << ( ( timeElapsed - interval ) / interval ) * 100.0 << "%";
            }
            yieldCounter = 0;
            importantBlockingFunction();
            // Reset the frame timer
            timeElapsed = ( double )timer.nsecsElapsed() / 1000.0 / 1000.0;
        }
        timer.start();
        // Running this just once means massive overhead from calling timer.start() so many times so quickly
        for( int i = 0; i < 100; i++ ) {
            yieldCounter++;
            QThread::yieldCurrentThread();
        }
        timeElapsed += ( double )timer.nsecsElapsed() / 1000.0 / 1000.0;
    }
}