How can I make my program sleep for a long time while staying responsive?

Viewed 324

I am working on a basic project. I have progressed a lot (first time). And I have encountered an obstacle while trying to make this as a working application.

What I mean by that is, it has a main loop which is infinite. The program will run until user presses q. By the way, the program is console application. And I'm working on Linux.

While the program is working, it needs to do some operation foo in intervals (eg. 1 hour, 30 minutes, 4 hours).

And yes that's it. I know it doesn't sound complicated but I'm stuck. So what I want is, a Stdin which waits for user input without blocking the application and during that time do foo for each 1 hour (let's stick with one hour). But between these foos, I don't want my program spin constantly. Because that causes 100% CPU usage. I want my program work when there is a need for work (eg. When elapsed time is 1 hour or When user presses q).

I used a library for manipulating the terminal. And that library has a feature for Stdin without blocking. And I looked it's implementation I saw that it creates another thread to handle this. Since I have to do other feature on my own. I need some help to figure it out.

For elapsing the time without spinning. I tried sleep main thread. It did help but it made user input not responsive. If the user presses q while main thread is sleeping. It waits for it to wake up. I don't want this. I want it to quit immediately if it is idle. (not doing foo). if it is doing foo, quit after foo is done. By the way foo is not a heavy computation in my code.

fn foo() {
    println!("bar");
}

fn main() {
    let mut instant = Instant::now();
    let one_hour = Duration::from_secs(60 * 60);
    let mut stdin = async_stdin().bytes();

    loop {
        // No need to work
        std::thread::sleep(one_hour);

        // Just to be sure
        if instant.elapsed() > one_hour {
            foo();
            instant = Instant::now();
        }

        // Check for input if there is any
        match stdin.next() {
            Some(Ok(b'q')) => break,
            _ => (),
        }
    }
}
    foo  stdin                          foo   stdin
     |     |                             |      |
     |     |                             |      | <- Press q
     |     |                             |      |
           |                                    | <- exit here
           |<- Press q and      or              |
           |   exit immediately                 |
           |                                    |
     |                                   |
     |                                   |
     |                                   |

I use Rust for my project but pseudocode is fine to me. I am very new to concepts of multithreading and asynchronous. I know they are not the same. I don't even know which is needed for this. Which synchronization objects do I need? Any tips, references, links will help.

3 Answers

You need to run 2 threads and pass messages about exit and wait them with timeout.

use std::sync::mpsc;
use std::thread;

type Signal = ();

fn work_queue(end_receiver: mpsc::Receiver<Signal>){
   loop{
      foo();
      match end_receiver.recv_timeout(one_hour){
         Ok(_) => return, // Received signal
         Err(mpsc::RecvTimeoutError::Timeout)=>foo(),
         _ => handle_error(),
      }     
   }
}

fn main(){
   let (send, recv) = channel();
   
   thread::spawn(move || {
    work_queue(recv);
   });
   loop{
      match stdin.next() {
            Some(Ok(b'q')) => {
               send.send(()).unwrap();
               break;
            },
            _ => continue,
      }
   }
}

I didn't test this code but I hope that idea is clear.

Here's an example of what you want using tokio.

use std::io::{stdin, BufRead, Result};
use tokio::time::{interval, Duration};

#[tokio::main]
async fn main() -> Result<()> {
    tokio::spawn(async {
        let mut interval = interval(Duration::from_secs(60 * 60)); // one hour
        interval.tick().await; // first tick fires immediately, ignore it
        loop {
            interval.tick().await;
            // do your stuff
        }
    });

    for line in stdin().lock().lines() {
        if line? == String::from("q") {
            break;
        }
    }

    Ok(())
}

The first bit spawns an async task that loops forever, waiting an hour between ticks. While the second bit just blocks on user input. Entering "q" immediately exits main and destroys the async executor. If the task is in the middle of processing, it will wait for it to reach the next await before exiting.

I am new to rust as well and I was interested in figuring out your question. I learned a lot coming up with this answer. This answer builds on the accepted answer.

Here is a complete and functioning example that addresses a common issue with reading the keyboard: Having to press enter before it reads the bytes()

I have also created multiple (two) event producers for mpsc and added the timer crate to handle timing.

Note: This probably won't run properly in vscode or jetbrains environment terminal as it depends on termion. Run it in a true terminal.

dependencies from cargo.toml

[dependencies]
timer = "0.2.0"
chrono = "0.4.19"
termion = "1.5.6"

main.rs

extern crate timer;
extern crate chrono;
extern crate termion;

use std::thread;
use std::sync::mpsc::channel;

use termion::input::TermRead;
use std::io::{stdout, stdin};
use termion::raw::IntoRawMode;
use termion::event::Key;

enum Events { TimerEvent, QuitEvent }

pub fn main() {
    let (tx, rx) = channel();

    let timer = timer::Timer::new();
    let timer_tx = tx.clone();

    let _guard = timer.schedule_repeating(chrono::Duration::seconds(3), move || {
        timer_tx.send(Events::TimerEvent).unwrap();
    });

    thread::spawn(move || {
        //This is the trick to reading input one key at a time
        stdout().into_raw_mode().unwrap();
        for input in stdin().keys() {
            if input.unwrap() == Key::Char('q') {
                tx.send(Events::QuitEvent).unwrap();
            }
        }
    });

    loop {
        let value = rx.recv().unwrap();
        match value {
            Events::TimerEvent => work_fn(),
            Events::QuitEvent => return  // return from main()
        }
    }
}

fn work_fn() {
    println!("Timer went off! Do work.");
}
Related