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?
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.
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:
async_delay() is called, but not awaited.
Some other work is done, like using select() in tokio, race() in smol, or performing some heavy computation.
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();
}
}
}
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();
}
}
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!