如何避免单线程事件循环中的繁忙等待



我正在编写一个简单的钢琴滚动应用程序,它使用Rust的SDL2绑定来进行事件处理和呈现。我有一些非常类似于下面的代码:

let fps = 60;
let accumulator = 0; // Time in seconds
'running: loop {
    let t0 = std::time::Instant::now();
    poll_events();
    if accumulator > 1.0 / fps {
        update();
        render();
        counter -= 1.0 / fps;
    }
    let t1 = std::time::Instant::now();
    let delta = (t1 - t0).as_secs_f64();
    accumulator += delta;
    // Busy wait?
}

总的来说,应用程序运行良好,没有任何明显的伪影,至少在我未经训练的眼睛里是这样。然而,CPU的使用是通过屋顶,使用近25%的平均(加上一些GPU使用渲染)。

我检查了一个非常相似的程序的CPU使用情况,它有更多的功能,也有更好的图形,当给予相同的MIDI音符显示时,它平均为2%的CPU使用,加上GPU方面的10%。

我还对我的代码进行了基准测试,并发现了以下近似时间:

  • poll_events(): ~0.002 ms
  • update(): ~0.1 ms
  • 渲染():~1 ms

考虑到我的目标是在逻辑层和渲染层都是60 fps,我大约有16毫秒来完成一个完整的轮询/更新/渲染周期。目前,我使用了大约2毫秒(很慷慨)的全范围,所以我的结论是主循环有一些繁忙的等待正在进行,我想摆脱。

我主要尝试了基于睡眠的解决方案,但非常不可靠,因为睡眠时间取决于操作系统,至少在我的机器(Windows 11)上大约是10-20毫秒,这会导致动画明显延迟。

据我所知,有一些与线程相关的解决方案可以避免这种情况,但我觉得这是一个完全没有必要的领域,因为我不需要任何并发性来从机器中挤出更多的性能。

我学习Rust已经有几个星期了,虽然我以前在一个使用c++的小项目中使用过SDL2,但我遇到了同样的问题,我找不到合适的解决方案。

我不确定这是否是一个与SDL2相关的问题,或者如果它也发生在使用其他库时,但任何帮助将非常感谢。

正如另一篇文章已经讨论过的,一些版本的Windows默认使用15ms的睡眠时间,但是操作系统确实有一个更精确的睡眠时间,可以配置到0.5ms。

有一个Rust板条箱,允许你通过使用操作系统计时器加上一点忙碌等待剩余的时间来访问更精确的计时器。也就是说,我没有使用这个功能,因为native_sleep()函数已经提供了我需要的分辨率。

更新后的代码看起来像这样:

let fps = 60.;
let accumulator = 0.; // Time in seconds
'running: loop {
    let t0 = std::time::Instant::now();
    poll_events();
    if accumulator > 1.0 / fps {
        update();
        render();
        counter -= 1.0 / fps;
    }
    // Fix
    let sleep_time = std::time::Duration::from_millis(1);
    spin_sleep::native_sleep(sleep_time);
    let t1 = std::time::Instant::now();
    let delta = (t1 - t0).as_secs_f64();
    accumulator += delta;
}

最新更新