Poll: Async/Await, let's talk about executors!

I don’t think just the library vs application distinction is that important, is there any reason you shouldn’t be able to abstract the parts of an application that need to spawn tasks out to a library?

As a concrete example http-service-hyper is a library that fully abstracts out what is necessary to run an http server, including spawning per request tasks. Being able to just pass in an impl Spawn to this library makes the interface simpler than if it gave back a stream of futures for you to spawn.

EDIT: I guess this could fall under the “very rare situations where it’s a good/necessary idea”, I just don’t think these are going to be as rare as you do.

2 Likes

Yes, that's a legitimate use case for spawning. And indeed we disagree on how common it will be, I think there will only be a handful of high-level APIs that need spawning, and pretty much everything else doesn't need/shouldn't use spawning.

Though in that particular case I think it would actually be better if it returned a Stream, since that gives you a lot more flexibility: you get to choose when or how to spawn, you can do things like throttle it, drop/debounce, divide the work across cores however you like, all sorts of good stuff.

Accepting impl Spawn means all of that is outside of your control (unless you create a custom Spawn implementation, which is more work).

One general solution that would work for both logs and async would be to parametrise modules:
https://github.com/rust-lang/rfcs/issues/424

That would allow a crate to be generic but declare that it needs something that extends a trait, and if that trait had methods on it that didn't have a self parameter, then anywhere in the module that method could be invoked without having to pass a parameter around.

5 Likes

I have been thinking about executor-access for libraries partly because it seems to me that this would be useful in order to poll-once when creating new tasks that need to wait on something external to the processor die (such as IO), in order to make sure that such operations are initiated immediately.

I think that there could be a mechanism that would give an async function some limited access to its own executor. This doesn't need to be global; an executing async function must necessarily be polled by an executor, so there can be no confusion about which executor should be accessible from the context of the function.

Unfortunately, I believe this would require some compiler magic to make work, or possibly an alternate form of poll that would pass in an executor.

I think this really goes against the philosophy of rust async. It is deliberate that futures don't do anything until polled.

In general you create your async workflow first (doing little processing to keep it fast) and then you start polling the system.

I think a feature providing an explicit polling mechanism would be compatible with that philosophy. It would generally only be useful for futures that do something very minimal on their first poll, such as initiate an asynchronous I/O operation. I think it would be particularly useful for single-threaded code that doesn't have work sharing.

2 Likes

I agree. There are plenty of good reasons to run futures in that sort of pre-emptive way.

Of course that will be done fully explicitly, and possibly with an explicit .await, so the default is still "don't run until Polled".

1 Like

I'm finally having a look at the nursery code to maybe create a crate with that functionality (or extend async_runtime). I have a few questions:

  • Just very technical, AssertUnwindSafe(...).catch_unwind. How is it possible to call that as a method. Looking at the docs and code of the stdlib, AssertUnwindSafe doesn't have a catch_unwind method. Nor does the FnOnce trait. How does this work?
  • How do we know that all futures are always unwind safe? Is it really safe to do this this way?
  • I would have all tasks polled concurrently, would I just use FuturesUnordered or would it be better to manually keep a bitmap tracking which ones are pending/complete and use that to decide which ones to poll. I remember you saying that FuturesUnordered might not be very optimized.

Thanks a lot for the inspiration in any case!

This is using FutureExt::catch_unwind via impl Future for AssertUnwindSafe<impl Future>.

I'm not 100% sure on what is required to be unwind safe, but by my understanding a spawned future can pretty much be assumed to be safe, from here there is a comment:

AssertUnwindSafe is used here because Send + 'static is basically an alias for an implementation of the UnwindSafe trait but we can't express that in the standard library right now.

Worst case that can happen is something like a panic or deadlock anyway, AssertUnwindSafe has a safe constructor so it's guaranteed sound to just apply it wherever you want.

You don't need to use FuturesUnordered or anything, the point of making the nursery generic over spawner is so that you remove any need to actually run any tasks and instead just handle keeping track of the tasks. That way you can use the full parallelism offered by any multi-threaded executor, while still scoping the tasks. (Whether it's possible to keep this part of the design and update the implementation to soundly allow actual scoped tasks that can hold references to stack variables, I'm not sure).

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.