如何避免单线程事件循环中的忙等待
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%。
我还对我的代码进行了基准测试,发现了以下近似时间:
- poll_events() : ~0.002 毫秒
- 更新():~0.1 毫秒
- 渲染():~1 毫秒
鉴于我的目标是在逻辑级别和渲染级别都达到 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;
}
我正在编写一个简单的钢琴卷帘应用程序,它使用 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 毫秒
- 更新():~0.1 毫秒
- 渲染():~1 毫秒
鉴于我的目标是在逻辑级别和渲染级别都达到 60 fps,我有大约 16 毫秒的时间来完成一个完整的 poll/update/render 周期。目前我正在使用整个范围的大约 2 毫秒(很慷慨),所以我的结论是主循环正在进行一些繁忙的等待,我想摆脱它。
我主要尝试过基于睡眠的解决方案,这些解决方案非常不可靠,因为睡眠时间取决于操作系统,至少在我的机器 (Windows 11) 中大约为 10-20 毫秒,导致动画明显延迟。
根据我的阅读,有一些与线程相关的解决方案可以避免这种情况,但我觉得这是一个完全没有必要进入的领域,因为我远远低于需要任何并发来挤压更多的点性能出机。
我已经学习 Rust 几个星期了,虽然我之前在一个使用 C++ 的较小项目中使用过 SDL2,但我遇到了同样的问题,但找不到合适的解决方案。
我不确定这是与 SDL2 特别相关的问题,还是使用其他库时也会发生,但我们将不胜感激。
因为另一个 post
有一个 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;
}