How to think about an empty loop{} in main() using 100%

Hi there.

For this code:

fn main() {
    loop {
    }
}

Activity Monitor.app shows the following usage percentage:

The reason I'm looking into this is because Gilrs (an rs gamepad input library) shows their getting started example using that same structure:

When using this loop{} construct and running my own program, I notice my laptop fans kicking into high gear. So, I'm wondering if there is a better way to consume the Gilrs events?

I don't have this type of issue when writing Node, or at least I've never had to use a loop{} construct to passively consume events, so I'm guessing I just have some kind of fundamental misunderstanding for Rust. My intuition is that such a simple program doesn't need to be the most CPU hungry thing on the machine, and I that I need to rate limit the loop in some way (as silly as that sounds).

I tried to search for similar topics about 100% CPU usage, but didn't find an answer; probably because this is a really basic question, and I'm just ignorant / missing something obvious?

If someone can help lift my ignorance here I would appreciate the help. I have a feeling the answer is going to be obvious but for whatever reason I can't think what to try next.

Thanks!

You should use std::thread::sleep in your loop to kill time, otherwise it will run as fast as your CPU can manage.

2 Likes

Okay, this makes sense. I feel uneasy about the ambiguity of throwing in a random amount of milliseconds to sleep in the middle of my code however. How do you know how many milliseconds to sleep? Does that change for each system with different CPU speeds?

Perhaps there is another way to do this that is more like consuming a JavaScript promise?

The timing doesn't change for different cpu speeds, in that it will never run faster than you specify but it may run slower.

Yes, you can use async functions in Rust, but somewhere deep down you have a loop like this driving everything, you just don't see it in some environments like Node (I have zero experience with Node, so I am basing that on your description).

Games tends to limit its update-per-second(UPS) in some arbitrary number, commonly 20 or 60 ups. For example in 20ups limit, each game loop has 50ms time budget and if it completes before 50ms it sleep() to pass the rest of time slots.

Your loop is doing exactly what you asked for. Running around, as fast as possible, forever, doing nothing. Of course the act of running around consumes all your CPU time.

Javascript has it's own "event loop" built into the language and run-time. So there is no need to code such a wait loop in JS. The run time is doing it for you.

Javascript is the only language I have come across that has such an even loop built in. Others, including Rust do not. Which means that if you run out off the end of main() then everything your program ever created is destroyed, everything stops and the program exits.

Hence you need to keep things "live" with a busy loop or by calling join() on threads and so on.

2 Likes

The gilrs example calls next_event inside the loop. This is a blocking call, so it will cause the thread to do nothing until a new event is available, which will prevent it from using 100% CPU.

If you are writing a game, your graphics library or game engine will probably have additional ways to sync your event loop to your input devices or output frame rate.

6 Likes

I'm going to have to look into this, first by trying the default Gilrs example, and then if that is not using 100%, see why my program that I based off the Gilrs example is not blocking. Thanks for explaining.

Thanks for all the replies.

I tried the example from the Gilrs docs, and it is showing 100% like the screen shot in the OP.

Adding the following sleep makes the CPU usage more like 1%:

use std::{thread, time};

fn main() {
    use gilrs::{Gilrs, Button, Event};

    let mut gilrs = Gilrs::new().unwrap();
    let ten_millis = time::Duration::from_millis(10);

    // Iterate over all connected gamepads
    for (_id, gamepad) in gilrs.gamepads() {
        println!("{} is {:?}", gamepad.name(), gamepad.power_info());
    }

    let mut active_gamepad = None;

    loop {
        // Examine new events
        while let Some(Event { id, event, time }) = gilrs.next_event() {
            println!("{:?} New event from {}: {:?}", time, id, event);
            active_gamepad = Some(id);
        }

        // You can also use cached gamepad state
        if let Some(gamepad) = active_gamepad.map(|id| gilrs.gamepad(id)) {
            if gamepad.is_pressed(Button::South) {
                println!("Button South is pressed (XBox - A, PS - X)");
            }
        }

        thread::sleep(ten_millis);
    }
}

So, @mbrubeck, is this library not blocking like you thought?

Thanks.

Yes, it looks like I was wrong about next_event blocking the thread.

I would suggest also trying https://doc.rust-lang.org/std/thread/fn.yield_now.html -- to me, that better communicates the intent than any particular sleep duration.

1 Like

std::thread::yield_now doesn't always block the thread. What it does is to ask OS to run another thread first if exist. So your CPU will still be spinning in 100%, activating OS thread scheduler for every loop in this time.

2 Likes

There's an open issue for adding a blocking next_event to gilrs.

1 Like

Hmm, I tried it out and you're right that it doesn't seem to lower CPU utilization.

Given this line in its description,

This is used when the programmer knows that the thread will have nothing to do for some time, and thus avoid wasting computing time.

I interpreted "give up a timeslice" to mean "wake soon, but not instantly", not "keep running if there are no other threads in the process" :slightly_frowning_face:

1 Like