std::this_thread::sleep_until 时间完全偏离了大约 2 倍,莫名其妙

Posted

技术标签:

【中文标题】std::this_thread::sleep_until 时间完全偏离了大约 2 倍,莫名其妙【英文标题】:std::this_thread::sleep_until timing is completely off by about a factor of 2, inexplicably 【发布时间】:2021-06-24 17:54:16 【问题描述】:

好吧,我真的不知道为什么会这样。我目前正在实现一个线程容器,它以分离的方式运行无限循环,每次迭代之间限制为一定的速度。

标题:

class timeloop

public:
    std::thread thread =  ;
    bool state = true;

    void (*function_pointer)() = nullptr;

    double ratio = 1.0f;
    std::chrono::nanoseconds elapsed =  ;

    timeloop(
        void (*function_pointer)() = nullptr
    );

    void function();
;

定义:

void timeloop::start()

    this->thread = std::thread(
        &loop::function,
        this
    );


void timeloop::function()

    std::chrono::steady_clock::time_point next;

    std::chrono::steady_clock::time_point start;
    std::chrono::steady_clock::time_point end;

    while (
        this->state
        )
    
        start = std::chrono::high_resolution_clock::now();
        next = start + std::chrono::nanoseconds(
            (long long) (this->ratio * (double) std::chrono::nanoseconds::period::den)
        );

        if (
            this->function_pointer != nullptr
            )
        
            this->function_pointer();
        

        /***************************
            this is the culprit
        ***************************/
        std::this_thread::sleep_until(
            next
        );

        end = std::chrono::high_resolution_clock::now();
        this->elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(
            end - start
            );
    

调用代码:

timeloop* thread_draw = new timeloop(
    &some_void_function
);
thread_draw->ratio = 1.0 / 128.0;

thread_draw->start();
thread_draw->thread.detach();

定义代码的行为很奇怪,特别是std::this_thread::sleep_until。对于this-&gt;ratio = 1.0 / 128.0,我预计帧速率约为128,startnext 的计算值强化了这一点,但它莫名其妙地徘徊在60 左右。是的,我尝试将next 除以2,但是这实际上使它下降到 40 左右。

验证正常睡眠时间的额外代码:

auto diff = std::chrono::nanoseconds(
    next - start
).count() / (double) std::chrono::nanoseconds::period::den;
auto equal = diff == this->ratio;

其中 equal 的计算结果为 true

帧率计算:

double time = (double) thread_draw->elapsed.count() / (double) std::chrono::nanoseconds::period::den;
double fps = 1.0 / time;

虽然我也使用外部 FPS 计数器来验证(NVIDIA ShadowPlay 和 RivaTuner/MSI Afterburner),但它们在计算值的大约 +-5 范围内。

而且我知道它是std::this_thread::sleep_until,因为一旦我将其注释掉,帧速率就会跃升至 2000 左右。是的...

我真的对此感到困惑,尤其是看到我找不到任何其他人曾经遇到过这个问题的证据。是的,我知道睡眠功能并不完全准确,而且肯定会时不时地出现打嗝,但持续睡眠几乎是预定时间的两倍是荒谬的。

我是否可能错误配置了编译器选项或其他什么?这绝对不是性能问题,而且我有理由确定这也不是逻辑错误(查看所有计算结果如何)[除非我在某处滥用 chrono]。

【问题讨论】:

通过将您的next 点设置为相对于now() 的呼叫,每次您失去绝对时间会给您带来的任何优势。也许这会有所帮助:***.com/questions/35468032/… 【参考方案1】:

我认为您的问题是您没有使用 absolute 时间,因为您一直在相对于当前时间 now() 重置 absolute 时间点 next

尝试改变这个:

while (
    this->state
    )

    start = std::chrono::high_resolution_clock::now();
    next = start + std::chrono::nanoseconds(
        (long long) (this->ratio * (double) std::chrono::nanoseconds::period::den)
    );
// ...

到这里

// before the loop
next = std::chrono::high_resolution_clock::now();

while (
    this->state
    )

    // keep the timing absolute by adding to the absolute time point
    next = next + std::chrono::nanoseconds(
        (long long) (this->ratio * (double) std::chrono::nanoseconds::period::den)
    );

// ...

这样你只调用一次now(),然后你所有的后续时间都是绝对(而不是相对)从那个点计算的。

编辑添加

另外,我会避免使用std::chrono::high_resolution_clock。它通常只是std::chrono::system_clock 的别名,它会随着系统时钟尝试与互联网时间保持同步而随机更改时间。

使用std::chrono::steady_clock

【讨论】:

过去就是这样工作的,遗憾的是它没有任何区别。此外,这可能会落后太多(如果 main 函数需要太长时间太频繁,或者可能是整个时间),此时它甚至不会限制任何东西。 @TARN4T1ON 在我看来,如果你想要有规律的时间,那么你想在那个时间做的工作必须是可行的。如果 main 函数花费的时间太长,那么您实际上无能为力,因为您的基本问题是 main 函数花费的时间太长......但是无论如何,您现在执行的方式将不可避免地漂移,因为它没有锚定到绝对时基。 @TARN4T1ON 我还添加了关于std::chrono::high_resolution_clock 的注释。你应该使用std::chrono::steady_clock 你说得对,我在试错阶段改了,没有改回来。但似乎睡眠比我记得的要糟糕得多(尽管我可以发誓几个月前它工作得很好)。感谢您的帮助。【参考方案2】:

不保证sleep_until的分辨率,你只能保证线程不会在时间点之前被唤醒。如果您正在实现主游戏循环,请阅读Fix your timestep。

使用睡眠来保证时间是一种糟糕的方法。您受到操作系统调度程序的摆布,例如我相信 Windows 的最小睡眠时间约为 10 毫秒。 (如果实现实际上要求操作系统使线程进入睡眠状态并且操作系统决定进行上下文切换。)

如果您调用glfwSwapBuffers 或类似名称,则绘图线程中的垂直同步也可能导致延迟。这可以解释为什么你的速度限制在 60FPS,但不能解释为什么评论 sleep 可以解决问题。

所以我的猜测是操作系统在上面的睡眠。我建议取消睡眠并依赖垂直同步,无论如何这都是您想要绘制的正确频率。与逻辑线程同步会很痛苦……但情况总是如此。

【讨论】:

这真的很奇特,因为我在不同的项目中使用了基本相同的代码,而且效果非常好。也在同一台机器上。绝对没有 10 毫秒的最小延迟,即使针对更高的帧速率(如 200+)也是如此。无论哪种方式,我从那篇文章中收集到的基本上是使用一个 while 循环,并且只做操作系统自己做的事情(即使它稍微贵一点)。 @TARN4T1ON 那就不确定了,你可以试着做一个简单的程序,在 main 中休眠,看看你能得到的最小数量是多少。 是的,这篇文章更注重物理,但总体上对于与时序相关的循环来说是一个很好的建议。它不使用睡眠正是因为它不可靠。 我实际上看了一下我第一次使用这种代码的项目,它也不再在那里工作了。这种滞后的构建也是如此。真的很时髦。老实说,可能是最近的一些更新,其他一些东西也停止了对我的工作(比如 Visual Studio 的性能分析器,我实际上想用它来更早地了解这个问题,但是是的......)。我将只使用while循环。谢谢! Here 是Fix your timestep 的高质量计时实现。

以上是关于std::this_thread::sleep_until 时间完全偏离了大约 2 倍,莫名其妙的主要内容,如果未能解决你的问题,请参考以下文章