Best practices when implementing a future

So, in the async-book it is stated that:

https://rust-lang.github.io/async-book/03_async_await/01_chapter.html

async bodies and other futures are lazy: they do nothing until they are run

That beeing the case, implementing a future like this, although possible, would be considered a bad practice ?

struct DoSomething;

impl DoSomething {
    fn new() -> Self {
        thread::spawn(|| {
            // Already working before beeing awaited
        });
        
        Self
    }
}

impl Future for DoSomething {
    type Output = ();
    
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        todo!()
    }
}

as oposed to something like this:

impl DoSomething2 {
    fn new() -> Self {
        Self { running: false }
    }
}

impl Future for DoSomething2 {
    type Output = ();
    
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if !self.running {
            self.running = true;
            thread::spawn(|| {
                //working
            });
            Poll::Pending
        } else {
            // ...
            Poll::Ready(())
        }
    }
}

This is basically tokio::task::spawn_blocking which does fire off the thread when creating the future. They've named the resulting future JoinHandle to make it clear that the only work the future is doing is receiving the result, which indeed, doesn't happen unless the handle is awaited. The work of generating the result isn't part of the future.

1 Like

The problem with the first choice is that futures are always cancel-able. Spinning up a thread then tearing it down without using the output is a relatively expensive waste.

However, the first choice is reasonable if the future is unlikely to be canceled and there is some benefit to data likely being available for the first poll.

In other words, were I in your shoes, I'd favor the second.

1 Like