Should the standard library have a basic Future runtime?

For discovery and user-friendliness reasons, should std include a basic, maybe multithreaded, future runtime? It seems strange to have to include a separate crate to even run a Future.

(As a separate note, that same argument applies to future combinators, which I personally believe should either be on the standardized Future trait, or at least in the standard library, but that's a separate discussion.)

  • Yes, include a future runtime
  • No, don't.

0 voters

No, don't, unless you mean future::block_on. The reasoning is that the futures runtime requires tradeoffs that the standard library doesn't want to have to make a choice. We can make e.g. runtime or similar a rust-lang crate, though, and potentially even distribute it with the standard distribution, depending on how things go.

But std shouldn't have anything beyond just a block_on runtime at most.

2 Likes

Putting an executor/runtime in the standard would put too much pressure on the maintainers, and they need to focus on more important things, like fixing the many soundness holes in Rust, or ICEs rather than dedicate time for a futures runtime. Executors can be very complex, and it would be best to leave it to the community to work things out.

But like @CAD97 said, having block_on should be fine, as that is quite small.

5 Likes

Yeah, block_on is very simple. I wrote a quick implementation a week or two back on a plane when I had nothing better to do.

static VTABLE: RawWakerVTable = RawWakerVTable::new(
    raw::clone,
    raw::wake_by_ref,
    raw::wake_by_ref,
    raw::drop,
);

mod raw {
    use super::{VTABLE, RawWaker, Mutex, Condvar};
    pub unsafe fn clone(data: *const ()) -> RawWaker {
        RawWaker::new(data, &VTABLE)
    }

    pub unsafe fn wake_by_ref(data: *const ()) {
        let &(ref lock, ref cvar) = &*(data as *const (Mutex<bool>, Condvar));
        let mut started = lock.lock().unwrap();
        *started = true;
        cvar.notify_one();
    }

    pub unsafe fn drop(_data: *const ()) {}
}

pub fn block_on<F: Future>(future: F) -> F::Output {
    let pair = (Mutex::new(false), Condvar::new());
    let raw_waker = RawWaker::new(&pair as *const _  as *const (), &VTABLE);
    let waker = unsafe { Waker::from_raw(raw_waker) };
    let mut cx = Context::from_waker(&waker);

    let &(ref lock, ref cvar) = &pair;

    pin_mut!(future);

    loop {
        match future.as_mut().poll(&mut cx) {
            Poll::Pending => {},
            Poll::Ready(val) => break val,
        }

        let mut started = lock.lock().unwrap();
        while !*started {
            started = cvar.wait(started).unwrap();
        }
    }
}

That implementation isn't memory safe, since it's always valid to call a Waker - even if the associated Future already has been polled to completion. That means Wakers must in practice either be refcounted or use a 'static/threadlocal object.

The implementation futures-rs uses std::thread::park, which satisfies the criteria and is probably as efficient as it gets.

2 Likes

Oops! Didn't realize that Waker could be called even after the completion of the Future.

There should be a list of "blessed" runtimes for the most common use cases, so cargo/rustc can point users towards them when it fails to compile an async program.

1 Like

Rather than having runtime, std library should define common trait for executor and/or runtime

1 Like

It does, Future along with the associated types in std::task are the interface between a future and the executor. The interface between an application and the executor doesn’t generally require abstraction as an application uses a single executor and can be written directly against it (and as far as I can think of the only “trait” that could be added is the equivalent of for<F: Future> FnOnce(F) -> F::Output which doesn’t provide much utility).

1 Like

Yes, in five years, when the dust has settled, and the backlog of RFC's to implement has gone down.

1 Like

That's not the same, so it doesn't.

Whether it is necessary is another question, but runtime != executor, runtime usually needs reactor also

I think there's some confusion. have published an async_runtime for futures 0.3. It has no reactor. It allows you to set a thread global executor per thread to decide whether you want the globally available rt::spawn method to spawn on the current thread or in a threadpool.

It all works, and you can even spawn network related futures from romio, and even from tokio. Every future is responsible for waking up the task when progress can be made, but what exactly that means really depends on the specifics of the future, and I think libraries generating futures should take care of that themselves.

It's true that for operating system IO, rust libraries often use mio. And both romio and tokio have a reactor object for interfacing between mio and the task system.

It could be argued that it would be best if there was generic reactor crate, that different IO libraries could use rather than every library roll their own. However it's not in my opinion part of the runtime directly.

Since it's an interface to the OS, something like mio or a reactor could one day also be in stdlib, but I think a first step is to have it as a crate that is generic enough for different libraries, and not tied to specifics of one of them.

In any case both can be independently supported.

2 Likes

@lachlansneff Just saw this blog post:

I haven't yet looked into how it works, but it has task::spawn as replacement for thread::spawn. There you have your standard library with a runtime!

4 Likes