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

How to avoid busy wait in a single threaded event loop

我正在编写一个简单的钢琴卷帘应用程序,它使用 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%。

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

鉴于我的目标是在逻辑级别和渲染级别都达到 60 fps,我有大约 16 毫秒的时间来完成一个完整的 poll/update/render 周期。目前我正在使用整个范围的大约 2 毫秒(很慷慨),所以我的结论是主循环正在进行一些繁忙的等待,我想摆脱它。

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

根据我的阅读,有一些与线程相关的解决方案可以避免这种情况,但我觉得这是一个完全没有必要进入的领域,因为我远远低于需要任何并发来挤压更多的点性能出机。

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

我不确定这是与 SDL2 特别相关的问题,还是使用其他库时也会发生,但我们将不胜感激。

因为另一个 post Windows 的某些版本默认使用 15 毫秒的休眠时间,但是 OS 确实有更精确的休眠时间,可以可配置为约 0.5 毫秒。

有一个 Rust crate 允许您通过使用 OS 计时器加上一点忙等待余数来访问更精确的计时器。也就是说,我没有使用此功能,因为 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;
}