Is there a way to sleep only with futures crate?

Apparently, sleeping is hard as it's not on the futures crate, or maybe it is too specific for the operating system wake mechanism for sleeping.

I'm trying to keep my dependencies count low, and I need just sleep for approximately 1 second, it does not need to be exact. Is there some kind of hack that I can do without adding one more crate to the project?

timers need a reactor to deliver the OS signal, so you need an async runtime for them to work properly anyway.

there's a crate called async-timer, but I don't know how well it works (comared to tokio::timer::Sleep or async_io::Timer (a.k.a. smol::Timer)).

why does the following do not work?

use futures::channel::oneshot;
use std::thread;

#[cfg(debug_assertions)]
pub async fn async_delay(duration: std::time::Duration) -> Result<(), ()> {
    let (tx, rx) = oneshot::channel();

    thread::spawn(move || {
        std::thread::sleep(duration);
        //TODO: treat error?
        tx.send(()).map_err(|_| ())
    });

    rx.await.map_err(|_| ())?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use futures::executor::block_on;

    #[test]
    fn test_async_delay() {
        println!("a");
        block_on(async {
            println!("begin");
            loop {
                println!("start delay");
                async_delay(std::time::Duration::from_secs(1))
                    .await
                    .unwrap();
            }
        });
    }
}

this should work. you essentially created your own reactor using a native thread. however, this implementation doesn't scale well because each timer needs its own thread. a more sophiscated implementation can use a timer scheduler/queue, sorted by the due times, which is roughly how async-io's timer is implemented.

1 Like

yes but it does not work, and I know it's simple that's why I enabled only for debug mode

It's just looping forever. The sleep is working properly. Test output is captured by default, so if you want the playground to show you its output, just use fn main.

I suggest rewriting your test to automatically verify the expected behavior:

#[cfg(test)]
mod tests {
    use super::async_delay;
    use futures::executor::block_on;
    use std::time::Duration;

    #[test]
    fn test_async_delay() {
        let started = std::time::Instant::now();

        block_on(async {
            async_delay(Duration::from_secs(1)).await.unwrap();
        });

        let elapsed = started.elapsed().as_secs_f32();
        let expected = 1.0..1.1;
        assert!(expected.contains(&elapsed));
    }
}

However, there’s a subtle issue in your sleep implementation. In Rust, async blocks are passive, meaning they don’t start executing until you explicitly await them.

Consider the following sequence:

  1. async_delay() is called, but not awaited.
  2. Some other work is done, like using select() in tokio, race() in smol, or performing some heavy computation.
  3. Finally, you await the future created in step 1. The timeout begins just now, though it should have started already in step 1.

To fix this, you can calculate a deadline when async_delay() is called and use that deadline when awaiting the future. Additionally, remove the Result<> type for the sleep function.

pub fn async_delay_fixed(duration: std::time::Duration) -> impl Future<Output = ()> {
    let deadline = Instant::now() + duration;

    async move {
        if deadline > Instant::now() {
            let (tx, rx) = oneshot::channel();

            thread::spawn(move || {
                let now = std::time::Instant::now();
                if deadline > now {
                    thread::sleep(deadline - now);
                }
                let _ = tx.send(()); // ignore error because it is perfectly ok when the receiver is dropped
            });

            rx.await.unwrap();
        }
    }
}

Here’s the complete code example:

Would this be ok?

pub fn async_delay_fixed(duration: std::time::Duration) -> impl Future<Output = ()> {
    let (tx, rx) = oneshot::channel();

    thread::spawn(move || {
        thread::sleep(duration);
        let _ = tx.send(()); // ignore error because it is perfectly ok when the receiver is dropped
    });

    async move {
        rx.await.unwrap();
    }
}

it passes your tests.

1 Like

It depends:

There’s a minor issue—perhaps more theoretical—that thread::spawn() takes a little time before the closure actually starts. So, the sleeping time essentially becomes spawn_latency + duration. Is this a problem? Maybe, maybe not. It really depends on your use case, such as system load or the sleep duration.

Another small consideration is that the thread gets spawned regardless of whether it’s needed. Again, is this an issue? Maybe, maybe not. As you guessed, it depends on your specific use case.

On the other hand, your solution is much simpler and quicker to read and understand, which is definitely a plus.

So, it works fine with regard to what the tests are checking. I’d say that counts as a yes!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.