Pre-Pre-RFC: Unsafe Futures

Right now, the only Future combinator (that I am aware of) for efficiently combining many futures is FuturesUnordered. However, it has a lot of overhead, due to synchronization and boxing.

I had an idea for a trait that could help: UnsafeFuture. The ideal definition of UnsafeFuture would be as follows:

trait UnsafeFuture {
    type Output;
    unsafe fn unsafe_poll(self: Pin<&mut Self>, waker: &mut Waker) -> Poll<Output>;
    unsafe fn unsafe_cancel(self: Pin<&mut Self>, waker: &mut Waker);
}

The key difference is that unsafe_poll is an unsafe fn. In particular, one must follow the following rules when calling it:

  • Once unsafe_poll has been called, one must neither drop the UnsafeFuture, nor free the underlying memory, until unsafe_poll has returned Poll::Ready
  • If one wishes to cancel an UnsafeFuture, one must call unsafe_cancel.

The advantage of such a design is performance. Since an UnsafeFuture is guaranteed to not be dropped prematurely, it can store buffers inside itself, as well as wakers of sub-futures. The drawback is that there is not much one can safely do with an UnsafeFuture. The main things one can do are:

  • .await it. (edit: This returns impl UnsafeFuture, not impl Future)
  • Spawn it on an executor
  • Call combinators on it.

What do the other programmers on IRLO think of this idea?

1 Like

Unfortunately, that's not quite true, because an async fn returns a plain impl Future. That one can be dropped to cancel, so it wouldn't be able to await an unsafe future on its stack.

Additionally, I don't see the point in providing a waker to unsafe_cancel if it needs to be synchronous (as it returns ()).

You don't have to write the RFC => I have already written it :slight_smile:

It's the same idea: Having Futures which run to completion, and can therefore do things that other Futures can't do. Or formulated a little bit different: Having async fns which behave a little bit more like normal functions.

I still think this would be helpful, for a variety of reasons. One of the more boring reasons is that a lot of async code I've recently reviewed would be broken in the case of cancellation, and being able to point people towards "this is a better default if you don't care about synchronous cancellation" could help and is a lot easier than trying to talk about Drop traits and what to do if synchronous drop does't work for their use-case.

Another reason is that it would unblock structured concurrency frameworks (like in Kotlin) without the downsides that so far turned some people off.

Therefore I'm looking to drive this further - but didn't had a lot of time to work on it recently. I had some more things to update in the RFC.

The difference between my version and what is described here is:

  • The name focusses on "what does it do" (RunToCompletion) instead of "how is it achieved" (Unsafe)
  • For graceful cancellation a side-channel in the form a parameter ( CancellationToken/StopToken) is used, instead of a separate method. That has various benefits:
    • It makes the mechanism more equivalent to what one would use with plain threads and functions. One can really transition from a threaded solution to an async one, and can keep the same cancellation mechanism.
    • It makes the change really minimal
    • It isn't too opportunated. If a library doesn't care about cancellation it will be fine. It it wants to offer support for structured concurrency (which it should :slight_smile: ) it can still do so.
  • The new Future type also gets an equivalent async fn type (or modifier). That is required to solve the issue that @CAD97 pointed out.

The drawback is that there is not much one can safely do with an UnsafeFuture . The main things one can do are:

  • .await it
  • Spawn it on an executor

I think these enough are fine. That's the same you can do with regular functions, and people don't typically say "functions are not good enough"

3 Likes

unsafe_cancel merely initiates cancelation, but you are correct that it need not have a waker of its own. You still are not allowed to drop an UnsafeFuture until it has returned Ready.

Under this RFC, existing async fn would continue to return impl Future, but async fn that used UnsafeFuture would return impl UnsafeFuture. impl Future would be a subtype of impl UnsafeFuture.

I made some comments, but I think this should be submitted as an RFC.

Indeed my first thought after reading this post was: "It would be cool if cancel returned Future" There are many cases when need for drop to be non-async creates some overhead, first thing that comes to mind is io_uring

Oh, that's makes sense. So no overhead of creating new future.