How often do you want non-send futures?

The answer is this:

If you call a function directly, you know precisely what future is being returned. Basically, we’re leaking details about the fn body, in some sense – or at least about the otherwise hidden type that it returns. This is a property of how impl trait works.

Essentially, when you write this:

fn foo() -> impl Future<..> { ... }

which is what an async fn desugars to, you are allowed to write a function that returns a non-send future or a send future. Callers of foo() cannot see exactly what type you returned, but they are able to tell if that type is Send or not.

In contrast, when you write code against a trait, you are being abstract over all implementors of the trait, so you can only assume the requirements that the trait states explicitly. So it might be that there are two impls, one of which returns a sendable future, and one of which doesn’t.

1 Like

Because trait methods are actually a family of methods who share a similar signature, and when you impose a rule on the trait method you impose that rule on every method in the family. But free functions or methods only descrobe themselves.

2 Likes

There’s another aspect to this, but I think it’s not the high-order bit. Still, an interesting wrinkle that @withoutboats raised.

If we did have a default of send, we can give better diagnostics. The problem is that now, you only detect that a future is sendable when you try to spawn a thread that is using it. This may be quite removed from the code that is preventing it from being sendable.

An example that @skade ran into (playground):

async fn foo() { 
  bar().await;
}
 
async fn bar() {
  let x = some_mutex.lock();
  baz().await;
}

async fn baz() { ... }

Here, the future returned by foo() is not send – this is because it embeds the future returned by bar(), and that future may have a mutex guard live across an await. Mutex guards are not sendable. Therefore, you get an error, because (in some states) the future may not be sendable across threads. But the error is fairly opaque, since it occurs at some point where we call foo(), but it has to do with the body of bar(). You can see it in the playground.

If we just required async fn bar() to return a send future in the first place, then we’d be able to give a more local error more easily.

Alternatively, though, we can probably overhaul the compiler and get super smart. =)

4 Likes

Oh, I should add that @cramertj says that, in Fuschia, they use a ton of single-thread executors, and so they are very concerned about a “send by default” design. Meant to include that in the original post.

10 Likes

It might be worth noting, possibly for use as a workaround with current impl, that when using what the async/await RFC calls the initialization pattern, you can add + Send to the return type where applicable.

Isn’t the initialization pattern, non-async fn with terminal async block, currently the only way to do this via trait methods?

2 Likes

As a less tongue-in-cheek version of this, I really do think that the right solution here is that we should work on improving the diagnostics for these types of scenarios. It’s not crazy to me to imagine this:

error[E0277]: `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
  --> src/main.rs:23:5
   |
23 |     is_send(foo());
   |     ^^^^^^^ `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
   |
   = help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
   = note: required because it appears within the type `for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}`
   = note: required because it appears within the type `[static generator@src/main.rs:13:30: 16:2 x:&std::sync::Mutex<u32> for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}]`
   = note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:13:30: 16:2 x:&std::sync::Mutex<u32> for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}]>`
   = note: required because it appears within the type `impl std::future::Future`
   = note: required because it appears within the type `impl std::future::Future`
   = note: required because it appears within the type `for<'r> {impl std::future::Future, ()}`
   = note: required because it appears within the type `[static generator@src/main.rs:9:16: 11:2 for<'r> {impl std::future::Future, ()}]`
   = note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:9:16: 11:2 for<'r> {impl std::future::Future, ()}]>`
   = note: required because it appears within the type `impl std::future::Future`
   = note: required because it appears within the type `impl std::future::Future`
note: required by `is_send`
  --> src/main.rs:5:1
   |
5  | fn is_send<T: Send>(t: T) {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^

becoming this:

error[E0277]: `foo()` cannot be sent between threads
note: the `is_send` function requires `T: Send`:
  --> src/main.rs:5:1
   |
5  | fn is_send<T: Send>(t: T) {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^
but in this invocation, the `foo()` is not `Send`
  --> src/main.rs:23:5
   |
23 |     is_send(foo());
   |             ^^^^^
because it is of type `impl Future<Output = ()>`
which contains an `std::sync::MutexGuard<'_, u32>`
note: pass `--full-error-type-info` for more information

It’s certainly not trivial, but something like this would improve a ton of other existing scenarios around impl Trait. I don’t personally think that async fn is particularly “special” here in comparison with other -> impl Trait functions. The error messages are similarly bad for -> impl Future / -> impl Iterator and this will be a problem again when we consider generators (gen fn).

I’d hazard a guess that requiring explicit Send opt-outs for generators is even more controversial than requiring them for Futures, yet I think it’s important that we be consistent between the two.

7 Likes

In case it wasn’t clear, I actually agree that it’s plausible we can do that. This is partly why I said this issue seemed less crucial to me. It’s just easier if we have a more local error, of course.

1 Like

Am I correct that anyone building on tokio, or using a framework like tide, will basically want all futures to be sendable?

That’s accurate.

How often will people want to have single-thread executors that make use of non-Send data?

For non-embedded systems using single-threaded executors is generally done for performance reasons – execution can be slightly sped up in some cases by not having to worry about threads.

But it’s desirable to be able to swap between executors with ease, which in practice means all Futures have a Send bound. This is the same for WebAssembly targets too.


I can’t comment on how this would interact with single-threaded non-std environments. @nemo157 might perhaps be able to share more on this.

3 Likes

My take is that futures returned by async fn in traits should be Send by default, with the ability to opt-out like this:

trait Trait {
    // Case 1: The future is `?Send`.
    fn foo(&self) -> impl Future<Output = ()>;

    // Case 2: The future is `Send`.
    async fn foo(&self);

    // Case 3: The future is `Send`.
    // Same thing as case 2, except written in a longer form.
    fn foo(&self) -> impl Future<Output = ()> + Send;
}

And here’s why. Consider this:

impl Struct {
    // The returned future borrows `arg`.
    async fn foo(&self, arg: &i32);

    // The returned future only borrows `self` and not `arg`.
    fn foo<'a>(&self, arg: &i32) -> impl Future<Output = ()> + 'a;
}

Async functions outside traits already have strong assumptions about borrowing by default (they borrow all arguments), and the only way to opt-out from that is to write the async fn in the longer fn -> impl Future form.

So it makes sense to me to likewise default to strong assumptions in trait methods when it comes to Send, with the ability to opt-out in the same manner by spelling out the signature in the longer form.

3 Likes

There are plenty of cases of Tokio being used with !Send futures. Tokio 100% supports that case and people use it. requiring all async fns to be Send is a non-starter IMO.

That said, Send is probably most common.

11 Likes

Which specific design are you referring to here? It’s been a long time since I remember seeing a full sketch of how async fn in traits could work. Wow, searching back for it the last major thread on this is over a year old:

There are (hopefully) soon to be implemented features (GATs + existential types) that will allow at least basic usage of async in traits, these don’t currently encounter the same issue because you can choose whether to add the Send bound to the associated type:

trait Process {
  type ProcessFuture<'a>: Future<Output = ()> + Send + 'a;

  fn process(&self) -> Self::ProcessFuture<'_>;
}

impl Process for () {
  type ProcessFuture<'a> = impl Future<Output = ()> + Send + 'a;

  fn process(&self) -> Self::ProcessFuture<'_> {
    async { println!("processing"); }
  }
}

fn foo<P: Process>(p: P) {
    let x = spawn(p.process());
}

The issue arises only once a new feature is added to allow the async fn syntax in traits (whether it’s direct sugar for the pattern above, or something different but similar).

I’ll second @cramertj here. I’m working on a Fuchsia-specific user-interface framework in Rust and it is strictly single-threaded. Having to add concurrency primitives just to use futures would be unappealing.

8 Likes

(Referred here by Taylor, I’m also on Fuchsia)

The biggest source of confusion for me was that Send is implicit, causing deferred non-local errors. Me and several others ran into the issue where a MutexGuard was used across an await point. Many of us couldn’t figure out why and needed to ask for help.

I like to see these errors before I actually schedule the methods on a multi-threaded executor. If I write a library, I want compiler errors even if the method is never used. Tagging/annotating a block of code, such as a file, type, trait or similar in one place as being Send (or not Send), would be helpful. (I don’t know enough about Rust to suggest any feasible such unit).

In a perfect world, the default wouldn’t be so terribly important, because it would be simple to change from one to another across a code base. For instance, going from a single-threaded to a multi-threaded executor is a valid use-case as a piece of code needs to be more performant.

14 Likes

I also find it really strange that this code:

fn foo() -> impl Foo { .. }

fn bar(f: impl Foo + Send) { .. } 

bar(foo())

Compiles if foo returns Send type and fails with compilation error otherwise. I believe such leakage of an inner implementation detail goes against Rust principles and we should use explicit impl Foo + Send in situations like this. Was this an intentional design? And if so was it discussed and what is rationale behind it?

As for async, I think we need a way to add additional trait bounds to return type, one way to do it is sync(Send + Foo) foo() -> T { .. } (unfortunately it’s a bit ugly… alternatively we could use attributes), which will be translated to fn foo() -> impl Future<T> + Send + Foo.

12 Likes

With respect to traits, there’s a somewhat “clever” thing that I can imagine “doing the right thing” in 95% of scenarios: a future returned from an async method (or gen fn method in the future) is Send IFF all the arguments to the method are Send and Sync (Sync because you want to also be able to hold references to the argument types within the Future), unless someone has manually annotated the async fn in the trait definition as Send or !Send (I don’t care particularly what the syntax for this is).

This will “do the right thing” in nearly all scenarios, excepting where a !Send type is acquired from “somewhere” outside of the arguments to the function. it’s never the case that an async fn with a !Send argument is Send (since at the very least the argument is held onto and dropped at the start of the Future). !Sync arguments are a bit trickier, but I’d imagine these would be extraordinarily rare in scenarios where the Future itself was expected to be Send.

WDYT?

8 Likes

Yes, this was always the design of impl Trait. Auto trait leakage was an intentional part of the design, and is necessary for many usecases where today it is impossible to represent a bound like impl SendIfAndOnlyIf<T: Send> explicitly.

3 Likes

Don’t think that solves well the common case where &self is a parameter, such as in the first example. For async fn process(&self) to be Send we’d need to write the bound Self: Sync.

In that case, the async fn future type would be Send if and only if Self: Send, which IMO is exactly what we want.

1 Like

I’ve used Futures and async fn in a few contexts, including the single-thread-focused Fuchsia codebase, writing Futures that perform thread-bound FFI code (i.e. a future which interacts with iOS Views on the main thread), and the more prevalent “do some server work and throw it in a work stealer”.

One question: does this same issue arise for generic async fns, where they have to specify the Send bound on their arguments if they want to be Send?

Another question: how does this differ from the need to annotate generic methods in traits with Send/Sync/Sized/'static bounds? That requirement is also inconvenient to type but is the status quo (perhaps relevant for consistency?).

If we model what async “desugars” to, does that give us a nice perspective for picking “Send required”/“Send not required” syntax?

async fn foo() -> String { ... }

// becomes roughly

fn foo() -> impl Future<Output = String> { ... }

// becomes roughly

fn foo<F>() -> F
where F: Future<Output = String>

// we can imagine adding *some* annotation which transitively turns this into

fn foo<F>() -> F
where F: Future<Output = String> + Send

Right? If this is the bound that conceptually we would emit from an annotation, is there a nice way to express this type bound in the source? Here’s a hasty idea:

async fn foo() -> String
where Send
{
    // ...
}

Yes, I was thinking about this. This seems connected to what I was saying that my suspicion is that this is more of a “project-wide default” (with exceptions), versus a default that applies equally across codebases.

This is not a proposal, but you could imagine something like #![async(?Send)] being used to toggle the meaning of async fn within a lexical scope between -> impl Future and -> impl Future + Send.

Alternatively, you could imagine using a procedural macro like #[async] fn foo() to desugar into async(?Send) fn foo() (or even having two procedural #[async], macros, such that you alter the import path from use async_send::async and use async_unsend::async or something like that.

Usually the danger of these sorts of “modes” is that copy-and-pasting code between e.g., stackoverflow can lead to surprising errors (if that code is written assuming ?Send or whatever).

Anyway, I’m just thinking out loud here.

1 Like