Simplest possible block_on?

Hi! As I'm learning about async Rust, I read Build your own block_on() by @stjepang, and Applied: Build an Executor from "Asynchronous Programming in Rust." There's quite a bit of nuance in even these didactic implementations. What I'd like to know is: what is the simplest possible implementation of block_on that will still drive any correct future to completion?

To make this easier, I'm thinking to add a few restrictions:

  • The implementation does not need to care at all about performance.
  • The implementation only needs to work in a single-threaded environment.
  • The implementation can assume it's the only executor on the system, if that helps.
  • The implementation doesn't need to support any kind of "spawn" functionality.

In this case I'm really only interested in a solution that's technically correct. The original motivation for this has been struggling to find a correct executor that will run on an embedded system.

Here’s my attempt from a few months ago. It should be correct as long as nothing else attempts to park or unpark the main thread.


Edit: I just cleaned this up to be more correct, simpler, and significantly less performant by ignoring the Waker and spin-waiting instead:

Playground

mod blocking_future {

    use std::future::*;
    use std::task::*;

    const PENDING_SLEEP_MS:u64 = 10;

    unsafe fn rwclone(_p: *const ()) -> RawWaker {
        make_raw_waker()
    }
    unsafe fn rwwake(_p: *const ()) {}
    unsafe fn rwwakebyref(_p: *const ()) {}
    unsafe fn rwdrop(_p: *const ()) {}

    static VTABLE: RawWakerVTable = RawWakerVTable::new(rwclone, rwwake, rwwakebyref, rwdrop);

    fn make_raw_waker() -> RawWaker {
        static DATA: () = ();
        RawWaker::new(&DATA, &VTABLE)
    }

    pub trait BlockingFuture: Future + Sized {
        fn block(self) -> <Self as Future>::Output {
            let mut boxed = Box::pin(self);
            let waker = unsafe { Waker::from_raw(make_raw_waker()) };
            let mut ctx = Context::from_waker(&waker);
            loop {
                match boxed.as_mut().poll(&mut ctx) {
                    Poll::Ready(x) => {
                        return x;
                    }
                    Poll::Pending => {
                        std::thread::sleep(std::time::Duration::from_millis(PENDING_SLEEP_MS))
                    }
                }
            }
        }
    }

    impl<F:Future + Sized> BlockingFuture for F {}
}
1 Like

Awesome, thanks for posting this! I ended up coming up with basically the same solution, except I used this crate cooked-waker because I was scared of the unsafe code with RawWaker. My solution looks like this:

use std::task::{Context, Poll};
use std::future::Future;
use cooked_waker::{IntoWaker as _, WakeRef};

struct NoopWaker;

impl WakeRef for NoopWaker {
    fn wake_by_ref(&self) {
        // It's okay to do nothing only because block_on continually polls
    }
}

fn block_on<F: Future>(future: F) -> F::Output {
    pin_utils::pin_mut!(future);
    loop {
        if let Poll::Ready(output) = future.as_mut().poll(&mut Context::from_waker(&NoopWaker.into_waker())) {
            return output
        }
    }
}

N.B. for anyone who stumbles onto this thread: while it is simple, making the waker a no-op is not likely what you want, because continually polling a future is exactly what Rust's async ecosystem is designed to avoid. Not only does this require 100% CPU utilization while doing no work, but I think it could also cause a future to build up a really huge set of wakers, which could be trouble for memory consumption.

3 Likes

According to the spec, Futures are only supposed to notify the waker from their mos recent poll call. There’s no actual enforcement for that, though, and I don’t know how many real-world implmentations follow that part of the spec.

Oh, thanks for pointing that out! I did not know that. Also, I realized that we're always giving it the same waker, so as long as it's storing the waker in a set-type data structure I think it should be okay.