Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do we need Send bounds to stabilize async_fn_in_trait? #103854

Closed
Tracked by #91611
tmandry opened this issue Nov 1, 2022 · 53 comments
Closed
Tracked by #91611

Do we need Send bounds to stabilize async_fn_in_trait? #103854

tmandry opened this issue Nov 1, 2022 · 53 comments
Labels
A-async-await Area: Async & Await AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. C-discussion Category: Discussion or questions that doesn't represent real issues. F-async_fn_in_trait Static async fn in traits T-lang Relevant to the language team, which will review and decide on the PR/issue. WG-async Working group: Async & await

Comments

@tmandry
Copy link
Member

tmandry commented Nov 1, 2022

Problem: Spawning from generics

Given an ordinary trait with async fn:

#![feature(async_fn_in_trait)]

trait AsyncIterator {
    type Item;
    async fn next(&mut self) -> Option<Self::Item>;
}

It is not currently possible to write a function like this:

fn spawn_print_all<I: AsyncIterator + Send + 'static>(mut count: I)
where
    I::Item: Display,
{
    tokio::spawn(async move {
        //       ^^^^^^^^^^^^
        // ERROR: future cannot be sent between threads safely
        while let Some(x) = count.next().await {
            //              ^^^^^^^^^^^^
            // note: future is not `Send` as it awaits another future which is not `Send`
            println!("{x}");
        }
    });
}

playground

Speaking more generally, it is impossible to write a function that

  • Is generic over this trait
  • Spawns a task on a work-stealing executor
  • Calls an async fn of the trait from the spawned task

The problem is that the compiler does not know the concrete type of the future returned by next, or whether that future is Send.

Near-term mitigations

Spawning from concrete contexts

However, it is perfectly fine to spawn in a non-generic function that calls our generic function, e.g.

async fn print_all<I: AsyncIterator>(mut count: I)
where
    I::Item: Display,
{
    while let Some(x) = count.next().await {
        println!("{x}");
    }
}

async fn do_something() {
    let iter = Countdown::new(10);
    executor::spawn(print_all(iter)); // <-- This works!
}

playground

This works because spawn occurs in a context where

  • We know the concrete type of our iterator, Countdown
  • We know the future returned by Countdown::next is Send
  • We therefore know that the future returned by our call to print_all::<Countdown> and passed to spawn is Send

Making this work smoothly depends on auto trait leakage.

Adding bounds in the trait

Another workaround is to write a special version of our trait that is designed to be used in generic contexts:

#![feature(return_position_impl_trait_in_trait)]

trait SpawnAsyncIterator: Send + 'static {
    type Item;
    fn next(&mut self) -> impl Future<Output = Option<Self::Item>> + Send + '_;
}

playground

Here we've added the Send bound by using return_position_impl_trait_in_trait syntax. We've also added Self: Send + 'static for convenience.

For a trait only used in a specific application, you could add these bounds directly to that trait instead of creating two versions of the same trait.

For cases where you do need two versions of the trait, your options are

  • If you control both versions of the trait, write a blanket impl that forwards from SpawnAsyncIterator to AsyncIterator (playground)
  • Write a macro that expands to a delegating impl for a given type
  • Write impls by hand for each type, depending on what you need

Only work-stealing executors

Even though work-stealing executors are the most commonly used in Rust, there are a sizable number of users that use single-threaded or thread-per-core executors. They won't run into this problem, at least with Send.

Aside: Possible solutions

Solutions are outside the scope of this issue, but they would probably involve the ability to write something like

fn spawn_print_all<I: AsyncIterator<next(): Send> + Send + 'static>(mut count: I)
//                                 ^^^^^^^^^^^^^^ new (syntax TBD)
where
    I::Item: Display,
{
    ...
}

Further discussion about the syntax or shape of this solution should happen on the async-fundamentals-initiative repo, or a future RFC.

Questions

  • How often do people see this problem in practice?
  • Is this problem, despite the mitigations, bad enough that we should hold back stabilization of async_fn_in_trait until we have a solution?

If you've had a chance to give async_fn_in_trait a spin, or can relay other relevant first-hand knowledge, please comment below with your experience.

@tmandry tmandry added T-lang Relevant to the language team, which will review and decide on the PR/issue. A-async-await Area: Async & Await C-discussion Category: Discussion or questions that doesn't represent real issues. F-async_fn_in_trait Static async fn in traits WG-async Working group: Async & await labels Nov 1, 2022
@eholk eholk added the AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. label Nov 7, 2022
@withoutboats
Copy link
Contributor

withoutboats commented Nov 17, 2022

(NOT A CONTRIBUTION)

I just read the blog post and I thought it would be a useful datapoint that I literally wrote code (using async-trait) that would have run into this issue earlier today. I spawn in generic code all the time; a common case is that there is some kind of dependency injection in order to mock code that would use the network for testing purposes; such code is an obvious use case for an async method (EDIT: and there are many other reasons to use generics for dependency injection, eg supporting multiple different transport protocols)

@Ralith
Copy link
Contributor

Ralith commented Nov 17, 2022

The async code I write is rarely generic, rarely spawns, and the combination is hence rare indeed. Although there are undoubtedly cases where this capability is a blocker, I think async traits are a large step forwards in usefulness without them, and it doesn't seem like the straightforward syntax proposed here will need breaking revision in the future.

@Sherlock-Holo
Copy link

the unsend future return by async trait is unusually, in the past people use the async trait crate and they are familiar that the future is send, if not, will mark at the trait as an attribute, and people use tokio or async std runtime in most cases, both of them are work stealing runtime, which require the future is send

@jplatte
Copy link
Contributor

jplatte commented Nov 18, 2022

I tried using this in axum a week or two ago. Because it does type erasure, it can only support either Send futures or non-Send futures and obviously the former is what most people want. This feature doesn't allow that restriction though and so I couldn't use it.

Edit: just saw the RPITIT mention in the blog post and I think that didn't work when I tried it, but I guess I should try again.

@withoutboats
Copy link
Contributor

withoutboats commented Nov 18, 2022

(NOT A CONTRIBUTION)

it doesn't seem like the straightforward syntax proposed here will need breaking revision in the future.

First, committing to the current behaviour means not having methods be send-by-default (opposite of the behaviour of async trait); maybe the team has already reached consensus on this, but it is a foreclosure on an option which is currently working well for the only actually used implementation of async traits. So it is a decision that is being made, not merely punted.

Even still, I don't think this is the only question at play here. The reality is that punted decisions have a tendency to stay punted for longer than initially expected. Async/await has already suffered reputational damage from how long its been a known-incomplete MVP and having cliffs like this very cliff. The team should consider what will be the effect of punting this issue.

In particular, the Send issue will create a much more difficult cliff. Currently the limitation is "async methods are not allowed in traits," which sucks but is easy to understand and plan for. Without resolving this, you will be using async methods just fine until suddenly you need a Send bound and you can't. These methods could be coming from an external library you don't control; you may need to refactor all of your code or you may need to drop using that library if you don't have a solution for this issue.

In my use case yesterday, the spawn was necessary to fix a bug in the code; if I couldn't've introduced it, I think we would have had to restructure our entire code base to fix that bug, because we used async methods before realising we would need to spawn in a generic context. This is not a trivial problem to run into.

@dwrensha
Copy link
Contributor

dwrensha commented Nov 18, 2022

EDIT: this post was based on me minunderstanding async methods and was corrected by withoutboats below. (click to see the original post

Even though work-stealing executors are the most commonly used in Rust, there are a sizable number of users that use single-threaded or thread-per-core executors. They won't run into this problem, at least with Send.

In capnp-rpc we use single-threaded futures, and it looks like we run into the same problem, except with 'static instead of Send.

Here's a distillation of the problem:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=0a984f5b66ea0333f74fb1b52f88dac0

My impression is that we're going to need a way to specify bounds on the returned futures of async trait methods, like:

trait MyServer {
    async (: 'static) fn foo(&mut self) -> Result<(),String>;
    async (: 'static) fn bar(&mut self) -> Result<(),String>;
}

I remember at some point in the past (like a few years ago) someone suggested that Send bounds should always be implicitly added. I think that's the wrong solution; it would prevent capnp-rpc from using async trait methods.

@withoutboats
Copy link
Contributor

(NOT A CONTRIBUTION)

@dwrensha That doesn't work because those methods are not 'static - they borrow from self. A future will be 'static if all of the input arguments to its method are 'static, which there is syntax to express (e.g. if those methods took self by value, T: MyServer + 'static would be sufficient). it doesn't introduce the same problem as Send.

@dwrensha
Copy link
Contributor

Ah, I see. I would have the same problem even if the async methods in question were not part of a trait.

more details (probably not relevant to the present issue)

We use this pattern, where a method has ephemeral access to &self and then returns a 'static future:

use std::future::Future;
use std::pin::Pin;

struct Foo {
    x: String
}

trait FooServer {
   fn foo(&self) -> Pin<Box<dyn Future<Output=()> + 'static>>;
}

impl FooServer for Foo {
    fn foo(&self) -> Pin<Box<dyn Future<Output=()> + 'static>> {
        // do stuff here that accesses &self.
        println!("{}", self.x);
        Box::pin(async {
            // no longer have access to &self.
            ()
        })
    }
}

pub fn main() {
    let foo  = Foo { x: "hi".into() };
    let fut : Pin<Box<dyn Future<Output=()> + 'static>> = foo.foo();
}

It would be nice if we could make foo() be an async method. I had mistakenly thought that the main obstacle was just that Rust does not allow async trait methods, but really the main obstacle is that I can't even write foo() as an async non-trait method, because as soon as it's an async method, the lifetime on self matches the lifetime on the returned future. I have no specific complaints that async methods work that way -- I had just forgotten about it.

@andrewgazelka
Copy link

andrewgazelka commented Nov 18, 2022

Not sure this is the right place to post this, but here are my opinions on...

type_alias_impl_trait Approach

From what I can tell, type_alias_impl_trait will fix this (as associated impl types can be referenced). This could be a workaround if async functions are forced to be desugared. However, this is a bad option because it decreases ergonomics and might not always be available when using a third-party library.

Related work

Kotlin has delegated properties and uses :: syntax for accessing hidden properties of fields (i.e., a field that has a custom getter and setter—it isn't really just a field). Perhaps Rust could have something similar as well for accessing properties of functions?

#![feature(async_fn_in_trait)]

trait AsyncIterator {
    type Item;
    async fn next(&mut self) -> Option<Self::Item>;
}

fn spawn_print_all<I: AsyncIterator + Send + 'static>(mut count: I)
where
    I::Item: Display,
    I::next::Return: Send
{
    tokio::spawn(async move {
        while let Some(x) = count.next().await {
            println!("{x}");
        }
    });
}

However, the I::next::Return: Send would be breaking, and I don't know what I think about the syntax. It shouldn't be breaking if people name their fields/types correctly. Maybe we could have a Rust edition to enforce this. This could also be a separate feature released after async traits.

Perhaps it isn't too weird, though—in Kotlinland,

let next = T::next;

would return a closure next of type |&mut T| -> Option<Self::Item>;.

@Ralith
Copy link
Contributor

Ralith commented Nov 18, 2022

A mechanism like @andrewgazelka outlines above for expressing bounds on what amount to anonymous associated types is an appealing general solution. Backwards-compatible syntax for such a feature is conceivable. Provided that something like that is possible, I think baking any particular set of bounds into the definition of async traits is a mistake that would be difficult to correct without breaking.

Blocking features that are useful today because someday they'll hopefully be even better feels weird, although I sympathize that we don't want to introduce architectural traps. On the other hand, software is never complete, and I'm very glad that async at large wasn't blocked on the introduction of, say, TAIT or generators.

@NobodyXu
Copy link
Contributor

P.S. It would be even better if we can have something like decltype to infer type of any expression.
It would then possible to infer type of lambda, impl trait, async fn, etc.
It would be possible to store the lambda or the future returned by async fn in a struct and name it.

@CobaltCause
Copy link

An experience report: I'm fooling around with making a web framework similar to warp but (among other things) using some fancy nightly features. Not being able to require Send while using async_fn_in_trait means I have to use either type_alias_impl_trait or return_position_impl_trait_in_trait in order to work around a compiler error caused by the code at this commit.

Compiler error message
error: future cannot be sent between threads safely
   --> src/lib.rs:159:44
    |
159 |     let server = Server::bind(&addr).serve(make_service);
    |                                            ^^^^^^^^^^^^ future created by async block is not `Send`
    |
    = help: within `hyper::proto::h2::server::H2Stream<impl Future<Output = Result<hyper::Response<Body>, Infallible>>, Body>`, the trait `Send` is not implemented for `impl Future<Output = Routing<<R as Router>::Response>>`
note: future is not `Send` as it awaits another future which is not `Send`
   --> src/lib.rs:142:42
    |
142 |                       let response = match routes
    |  __________________________________________^
143 | |                         .route(&mut State::default(), &request)
    | |_______________________________________________________________^ await occurs here on type `impl Future<Output = Routing<<R as Router>::Response>>`, which is not `Send`
note: required by a bound in `hyper::server::Builder::<I, E>::serve`
   --> /home/charles/.cargo/registry/src/github.com-1ecc6299db9ec823/hyper-0.14.23/src/server/server.rs:548:12
    |
548 |         E: ConnStreamExec<<S::Service as HttpService<Body>>::Future, B>,
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `hyper::server::Builder::<I, E>::serve`
help: consider further restricting the associated type
    |
132 |     H: IntoResponse, impl Future<Output = Routing<<R as Router>::Response>>: Send
    |                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

(Also that "consider further restricting the associated type" suggestion obviously doesn't work.)

Personally, I don't know if this is worth delaying stabilization since having async_fn_in_trait in any capacity is probably better than not at all, and I think the best way to solve this would be to have some way to require bounds on the future in calling code which wouldn't be a breaking change. Basically, I'm in agreement with what Ralith wrote above.

@xmh0511
Copy link

xmh0511 commented Nov 21, 2022

For the associated type, when we want some constraints to that type, we may write

fn show<U, T: Trait<Output = U>>(v:T) where U: ???

Similarly, we may design the trait bound for the associated function/method in a similar way, which looks like

fn show<P0, P1, R, T: Trait< next(P0, P1 /*... Pn*/)->R >>(v:T) where P0: ???, P1:???, R: Send
{}

which can have better fine-grained control over the corresponding types of the associated function/method. Furthermore, we can reuse the current syntax without creating any new syntax.

fn show<P0:???, P1, R:Send, T: Trait< next(P0, P1 /* ... Pn */)->R >>(v:T)
{}

For the method, I think

fn show<P, R, T: Trait< next(/* & */T, P) -> R > >(v:T)
                            /* & mut */
{}

might also be clear.

@tamasfe
Copy link
Contributor

tamasfe commented Nov 21, 2022

I'm writing from a user's perspective who uses async on a daily basis in web contexts, where taking advantage of parallelism via spawning tasks to multiple threads is the norm. Just after a day of playing around with this feature I've run into the lack of Send bound twice by now with ugly workarounds (tokio's LocalSet) or no possible workaround whatsoever.

I suspect many people are just looking to be able to drop async-trait to avoid boxing (due to its overhead and due to use in no-std contexts), to avoid IDE quirks due to the macro, and to get rid of a dependency when traits are exposed from libraries.

Generally I would expect the following after stabilization:

you can now drop #[async_trait], but your trait will not be object-safe

Without the ability to specify bounds the saying above will not hold true which is made worse by the fact that async-trait's futures are all Send by default.

I think allowing RPITIT to specify bounds and lifetimes in trait definitions even if just restricted to Futures would allow me to use async_fn_in_trait (AFIT?) in places where I would use them now. If I need object safety, I can simply use the type erasure techniques that already exist.

I think specifying bounds of return types (e.g. with decltype/typeof) is orthogonal, and requires a lot more bikeshedding and implementation work whereas RPITIT is mostly complete from what I see.

decltype syntax bikeshedding

While I don't think decltype is a solution in the foreseeable future, I did some thinking about the syntax I would like.

/// All the examples mean the same, they restrict
/// the return type of the given functions.
trait Foo
where
    typeof(Self::foo()): Send,
{
    async fn foo(&self);

    async fn bar(&self)
    where
        typeof(return): Send;

    async fn baz(&self)
    where
        typeof(baz()): Send;
}

@andrewgazelka
Copy link

andrewgazelka commented Nov 22, 2022

More bikeshed

To bikeshed some more, I think removing ()s makes sense as they are not needed. This is how Kotlin references functions as types

/// All the examples mean the same, they restrict
/// the return type of the given functions.
trait Foo
where
    typeof(Self::foo): Send,
{
    async fn foo(&self);

    async fn baz(&self)
    where
        typeof(baz): Send;
}

@Nugine
Copy link
Contributor

Nugine commented Nov 22, 2022

Inspired by #103854 (comment). What if we can specify bounds of associated functions?

#![feature(async_fn_in_trait)]

trait AsyncIterator {
    type Item;
    async fn next(&mut self) -> Option<Self::Item>;
}

fn spawn_print_all<I: AsyncIterator + Send + 'static>(mut count: I)
where
    I::Item: Display,
    I::next: for<'a> impl FnOnce(&'a mut I) -> impl Future<Output = Option<I::Item>> + Send + 'a
{
    tokio::spawn(async move {
        while let Some(x) = count.next().await {
            println!("{x}");
        }
    });
}

It seems too verbose.

@dojiong
Copy link

dojiong commented Nov 22, 2022

@Nugine Fn has associated type: Output, it may simplify to <I::next as Fn<...>>::Output: Send

@xmh0511
Copy link

xmh0511 commented Nov 22, 2022

Inspired by #103854 (comment). What if we can specify bounds of associated functions?

#![feature(async_fn_in_trait)]

trait AsyncIterator {
    type Item;
    async fn next(&mut self) -> Option<Self::Item>;
}

fn spawn_print_all<I: AsyncIterator + Send + 'static>(mut count: I)
where
    I::Item: Display,
    I::next: for<'a> impl FnOnce(&'a mut I) -> impl Future<Output = Option<I::Item>> + Send + 'a
{
    tokio::spawn(async move {
        while let Some(x) = count.next().await {
            println!("{x}");
        }
    });
}

It seems too verbose.

The syntax I::next: Trait would be a bit misleading since I::next is not a type or type alias. It is better if we would write that

decltype(I::next): Trait

and therefore we could also write the requirement as

<decltype(I::next) as Fn<...>>::Output: Send

pointed by @dojiong

@andrewgazelka
Copy link

andrewgazelka commented Nov 22, 2022

thoughts on

decltype<T>() -> Type<T>

similar to discriminant but not requiring a value? Type would be compile generated to be equivalent to the return type

@tamasfe
Copy link
Contributor

tamasfe commented Nov 22, 2022

(I do not work on the compiler, and I do not get to decide anything, the below text is only an opinion as an outside observer.)

I can't help but see that this thread is turning into a decltype bikeshedding discussion, I have a few additional thoughts on that:

decltype is difficult
  • If we really want that, it should probably get its own issue/thread and then an RFC.
  • It should probably use the typeof keyword, as it is already reserved.
  • It should have similar semantics to the C++ counterpart, thus if foo is of type fn() -> T, typeof(foo) should resolve to fn() -> T and typeof(foo()) should resolve to T.
  • It should be able to evaluate expressions, e.g. typeof(foo + 1).
  • It should be able to be used in trait bounds, I imagine it's a completely different beast.

This is just me playing around with the idea without all edge cases considered. I imagine properly designing and implementing this feature would push async traits way beyond the estimated 6 months in the async trait blog post.

I do believe that

  • specifying bounds (Send) on return types is important for async fns in traits to be useful for me and probably many others
  • a complete decltype implementation is not in scope of this issue
  • special syntax for naming return types in bounds (e.g. via ::Return) looks viable
  • RPITIT seems to be already implemented, unless there is a reason why it shouldn't be allowed, it looks like an obvious next step to me

@andrewgazelka
Copy link

(I do not work on the compiler, and I do not get to decide anything, the below text is only an opinion as an outside observer.)

I can't help but see that this thread is turning into a decltype bikeshedding discussion, I have a few additional thoughts on that:

decltype is difficult
I do believe that

  • specifying bounds (Send) on return types is important for async fns in traits to be useful for me and probably many others
  • a complete decltype implementation is not in scope of this issue
  • special syntax for naming return types in bounds (e.g. via ::Return) looks viable
  • RPITIT seems to be already implemented, unless there is a reason why it shouldn't be allowed, it looks like an obvious next step to me

I agree with the bikeshedding.

Honestly, I'd like to see async_fn_in_trait out as soon as possible. I am a firmware engineer, and naturally—as it is often run on a single core—firmware code is much easier to write with async_fn_in_trait being stabilized. This is even more pressing as Boxes do not exist in no_std environments, so #[async_trait] cannot be used. The sooner this transition is made, the less HAL code will need to be rewritten. See embassy, which currently requires nightly.

@Nugine
Copy link
Contributor

Nugine commented Nov 22, 2022

For now, if you want to define an "async" trait, you may use:

  • trait + named/boxed future + ?Send (stable)
  • GAT1 + named/boxed future + ?Send (stable)
  • GAT + associated future type + ?Send (TAIT2)
  • GAT + anonymous future + ?Send (RPITIT3)
  • GAT + async fn

We alreay have async_trait4 syntax widely used through proc macro. The macro can desugar to one of the five methods above when new features are stabilized. Only the last method has a "Send" problem. The solution is not trivial.

So I think we should solve the "Send" problem before stabilizing async_fn_in_trait.

Footnotes

  1. GAT, generic_associated_types

  2. TAIT, type_alias_impl_trait

  3. RPITIT, return_position_impl_trait_in_trait

  4. https://docs.rs/async-trait

@withoutboats
Copy link
Contributor

withoutboats commented Nov 24, 2022

(NOT A CONTRIBUTION)

I am not at liberty to post an example, perhaps someone else could come up with one.

Structured concurrency isn't relevant here: even if Rust had chosen to push structured concurrency and had frameworks that don't provide spawn without a nursery you would still have the same problem: to make sure a task completes, you would need to pass a nursery into that context and spawn onto it. The nursery's spawn method would require Send, and you'd be stuck with the same problem.

Regardless, Rust didn't push structured concurrency and with the existing frameworks spawning is not an anti-pattern.

@withoutboats
Copy link
Contributor

withoutboats commented Dec 8, 2022

(NOT A CONTRIBUTION)

Another experience report: today I encountered an issue in which a minor refactor caused rustc's overly conservative analysis to believe a shared reference to a future returned by an async-trait async method had to be stored in an outer async function's future state, and therefore the future returned by that method needed to be Sync in order for the outer future to be Send. Once I tracked down how this happened I was able to do a further small refactor (creating a let binding) to fix it.

async-trait doesn't provide a way to say that methods' return types must be Sync; if the further refactor weren't possible I would have been up against a wall here just as I would be with Send without a resolution to the issue here. I think it's a lot less likely for that to happen for Sync than Send - its very rare to need to hold a shared reference to a future across an await point for correctness - but maybe it can. So I wanted to add that it seems to me that supporting Sync (which async-trait doesn't do) is important for completeness.

This leads me toward the solution the team seems to favour - a syntax in where clauses for referring to the return type of an async method - which is flexible enough to support both Send and Sync (and for that matter possibly other traits that people want, like Copy or Clone maybe), as opposed to async-trait's solution (just always being Send and having a syntax for opting out of Send specifically).


And one other unrelated thought: spawn is not the only way you may need to introduce a Send bound, another way is when you want to deal in Box<dyn Future>, at which point you have to decide on whether or not the trait object will be + Send or not instead of eliding it until final instantiation. I think it's okay to say async methods aren't object safe for the trait in the MVP, but making it impossible to cast async method return types into boxed send futures is another cost beyond spawning.

@George-Miao
Copy link

I think, in order to increase the fine-grained control over the asyn function in trait, we may design the syntax as the following:

Option 1:

fn use_trait<T>(_:T)
where T: MyTrait<do_some_thing = for <Params, Return:Send> fn(Params)->Return where Params: Send >
{
}

Option 2:

fn use_trait<T, U, Params, Return>(_:T)
where T: MyTrait< Output = U, do_some_thing = fn(Params)->Return >  // consistent
Params: Send,
Return: Send
{
}

which can let us specify the trait on any type in the async function, and it also keeps consistent with the manner we used to specify the trait bound on the associated type. The benefit of first option is that we can also specify the lifetime parameter if we want.

Option 3:

fn use_trait<T, U, Params, Return>(_:T)
where T: MyTrait< Output = U, do_some_thing = for<'a> fn(&'a Params)->Return >  // consistent
Params: Send,
Return: Send
{
}

The third option can have both the advantages of the first and the second options.

decltype or typeof is wonderful but it will introduce some complex, in which how to specify the arguments in a function call is a problem.

decltype(fun(arg0, argu1)); // how ??

But most of the time we should already know types of params in functions. And corresponding trait bounds should be imposed on type params of traits or functions instead of assigning a param variable and impose again. I think this is kind of repeating what you have already said (the types). If I'm understanding right, opaque returned futures are the only things we are interested in?

@tvallotton
Copy link
Contributor

tvallotton commented Dec 15, 2022

I think making Send the default is a mistake, mostly because I think work stealing async executors are a mistake too. Currently, rust's async ecosystem is extremely vendor locked, which strengthens tokio's monopoly over it. I expect that when async traits get stabilized, tokio's popularity will decline in favor of other runtimes, and these may even supersede it. It would be unfortunate to continue to tailor rust's async design to tokio's needs, when other executor architectures have been proven to be faster and easier to use.

@andrewgazelka
Copy link

andrewgazelka commented Dec 15, 2022

when other executor architectures have been proven to be faster and easier to use.

@tvallotton source/refs? what other runtimes do you prefer? this argument seems pretty subjective. I don't think anything can be "proven" to be easier to use...

@tvallotton
Copy link
Contributor

Well, I consider single threaded executors easier to use because the don't require me to place Send bounds everywhere when I write async code. Now, if I'm using tokio, I'll usually give up pretty quick and use a Mutex where needed, because I understand the error and I understand how to fix it. However, I believe beginners usually have a harder time with this requirement.
And in general, I'm not a fan of work stealing. I think work stealing is the kind of thing that makes sense in a language where everything has to be synchronized anyway for safety.

@tvallotton
Copy link
Contributor

To explain myself a little bit more with respect to my opinions around work stealing. Suppose I wrote some function with rayon that access a shared mutex. You surely would think, that is silly, using a mutex goes actively against my goal of parallelizing my code. In fact, all that contention would likely slow it down. But that is what we often write when we use tokio. Because tokio forces Send bounds onto us, then we are forced to synchronize all our shared mutable state.
If we instead used a single threaded executor, and used rayon in truly parallelizable tasks, then we would save ourselves all that needless synchronization.
(Also forgot to ping @andrewgazelka)

@CobaltCause
Copy link

I think making Send the default is a mistake

Correct me if I'm wrong, but I don't think anyone was suggesting this. All the discussion I see is about how to require Send on async fns from traits in generic contexts, not whether all async fns in traits should always be Send.

@tvallotton
Copy link
Contributor

Oh yeah sorry, I was mostly commenting on this paragraph by @withoutboats, which I interpreted as proposing Send by default. But as I read it again it seems they were just pointing out that this option existed.

First, committing to the current behaviour means not having methods be send-by-default (opposite of the behaviour of async trait); maybe the team has already reached consensus on this, but it is a foreclosure on an option which is currently working well for the only actually used implementation of async traits. So it is a decision that is being made, not merely punted.

@NobodyXu

This comment was marked as off-topic.

@NobodyXu

This comment was marked as off-topic.

@andrewgazelka
Copy link

andrewgazelka commented Dec 15, 2022

To explain myself a little bit more with respect to my opinions around work stealing. Suppose I wrote some function with rayon that access a shared mutex. You surely would think, that is silly, using a mutex goes actively against my goal of parallelizing my code. In fact, all that contention would likely slow it down. But that is what we often write when we use tokio. Because tokio forces Send bounds onto us, then we are forced to synchronize all our shared mutable state. If we instead used a single threaded executor, and used rayon in truly parallelizable tasks, then we would save ourselves all that needless synchronization. (Also forgot to ping @andrewgazelka)

P.S. tokio supports spawning on single thread (without Send bound) using tokio::task::spawn_local as long as you have an active tokio::task::LocalSet.

To be fair to @tvallotton many tokio primitives are Send, i.e., their channels. Regardless, I think we are going off-topic.

@junderw
Copy link

junderw commented Dec 16, 2022

  • How often do people see this problem in practice?

Rarely do I need to spawn inside a generic function, let alone one that is generic over an async trait.

  • Is this problem, despite the mitigations, bad enough that we should hold back stabilization of async_fn_in_trait until we have a solution?

The mitigations seem sufficient, as long as compiler errors / clippy can lead users to the mitigations. I don't believe that Send should be a default requirement or require some special syntax to shorthand it.

However, that being said, I do believe there is benefit to new people using the language to make it easier to make things work without over-complicating trait signatures. So I am not completely against the idea, as long as it makes sense and the related errors are helpful.

@CobaltCause
Copy link

The mitigations seem sufficient, as long as compiler errors / clippy can lead users to the mitigations

The problem is that the mitigation, today, requires the ability change how the trait is written. If the trait comes from a different crate, you have no recourse.

I don't believe that Send should [...] require some special syntax

The only way to solve the above problem is by adding new syntax.

@junderw
Copy link

junderw commented Dec 16, 2022

The problem is that the mitigation, today, requires the ability change how the trait is written. If the trait comes from a different crate, you have no recourse.

That is a problem with the design of that API. If the users of that API want Send bounds they should provide their input to the API designers, and they should use a mitigation.

That should not be the concern of the language.

In the end, even if the default was a Send bound and some simplified syntax to remove the bound was added, API designers could still get it wrong.

@CobaltCause
Copy link

I don't believe that Send should be a default requirement

In the end, even if the default was a Send bound and some simplified syntax to remove the bound was added, API designers could still get it wrong.

Again, I don't think anyone is suggesting this.

That is a problem with the design of that API. If the users of that API want Send bounds they should provide their input to the API designers, and they should use a mitigation.

That should not be the concern of the language.

Not being able to require Send on a Future returned by an async fn from a trait in a generic context is a shortcoming of the language, right? So then, shouldn't the language change to obviate this shortcoming? The language created this problem, so the language should solve it. There's no way to do this in user code...

... Aside from using different language features (i.e. "a mitigation", e.g. rewrite the trait using TAIT, i.e. make a breaking change), but this doesn't solve the problem, it just avoids it. I don't think "use a mitigation when necessary" is practical because, with new syntax to support requiring Send on a Future returned by an async fn from a trait in a generic context, async fns:

  • are much easier to write
  • don't necessitate a feedback loop between API authors and users
  • don't have the potential for the API authors to accidentally exclude some use cases in the first place

@jplatte
Copy link
Contributor

jplatte commented Dec 16, 2022

don't have the potential for the API authors to accidentally exclude some use cases in the first place

I want to expand on this: Having the ability to enforce the Sendness of an associated future type of a trait is one of the main reasons I wanted to try this feature out in axum, and I was pretty disappointed to find that it just isn't possible right now, except by using TAIT plus a named associated type, but then implementers of the trait can't use async fn syntax anymore.

The use case for it is this: Ideally, axum(-core)'s traits would not require implementers of its FromRequest and FromRequestParts traits to have Send futures for the extraction functions, but axum's current Router boxes futures that include these as dyn Future + Send + …. If the proposed feature was available, the traits and many implementations could be shared both by the current Router as well as alternative experimental routing designs optimized for single-thread usage (at one point, the aws-smithy-http-server crate was a fork of axum tailored towards single-threaded usage, though it seems to have diverged and I'm not sure whether they would still benefit from using axum-core).

@withoutboats
Copy link
Contributor

(NOT A CONTRIBUTION)

That should not be the concern of the language.

This just isn't how Rust works. Trait bounds are composable so downstream users can impose multiple intersecting requirements on their types, including on the associated types of traits. E.G. users can write Iterator<Item: Send> when they need an iterator of sendable items. This would be the only exception to that, and it would make async methods far less useful. I am sure no one on the language team thinks that there is no need to ever provide a way to add bounds the return type of an async method, the question in this thread is whether or not it is a blocking concern on stabilising async methods.

@withoutboats
Copy link
Contributor

withoutboats commented Dec 16, 2022

(NOT A CONTRIBUTION)

A query: looking at the code sample for the RPIT "workaround," it looks like there's no way to use RPIT to have a single trait with an async method return type that could or could not be Send, is that right? Because there's still no way to name those types and add a Send bound in a where clause, you can only specify in the trait signature that it's Send.

So if the feature were stabilised as is, the workaround would be to always provide a second version of the trait which requires Send, and then implementers should always implement this alternative trait if their implementation is Send. However if there's a blanket implementation, you'll run into problems when an implementer is itself generic and is only Send if its generic arguments are Send (e.g. implementing an async trait for Vec<T>): if you implement both the normal version for all Vec<T> and the send version for Vec<T: Send>, you'll end up with conflicting impls.

So you can't have the blanket implementation. Instead you need something like SpawnAsyncIterator: AsyncIterator + Send and implementers need to manually implement both. (EDIT: Actually, this doesn't even work, see below.)

Since library authors can't know how their libraries will be use, this would then be the best practice to make sure their library is useable by all their downstream users:

  • If a library author is defining a trait with an async method, they also need to define a send version of that trait, which is basically the same interface but all async methods replaced with RPIT methods with send bounds
  • If a library author is implementing a trait with an async method for a type, unless the implementation is definitely not Send, they need to implement it twice, once for the not send version and once for the send version.

This workaround is a lot of additional complexity for users, and I think it will just exacerbate the impression that async Rust is full of bizarre pits to fall into as a user and be confronted with type nonsense you don't understand or care about. It sounds like the team already has consensus about the syntax they want, unless there's some huge problem with implementing return type bounding, I really think you should just do this correctly.


EDIT: Actually, the workaround does not work without the blanket implementation.

Without the blanket implementation, there's no guarantee that T: SpawnAsyncIterator implies <T as AsyncIterator>::next(): Send. So the blanket implementation is necessary to e.g. call print_all from spawn_print_all in the example in the post.

But the blanket implementation means that generic types will not be able to implement SpawnAsyncIterator conditional on their generics being Send and also AsyncIterator when their generics are not Send. So generic types will either only be able to implement the Send version or not able to implement the Send version at all.

So the workaround is not just more boilerplate and a more confusing interface, but is actually not able to cover all the use cases Send bounds would cover.

@GunnarMorrigan
Copy link

(NOT A CONTRIBUTION)

I just read the blog post and I thought it would be a useful datapoint that I literally wrote code (using async-trait) that would have run into this issue earlier today. I spawn in generic code all the time; a common case is that there is some kind of dependency injection in order to mock code that would use the network for testing purposes; such code is an obvious use case for an async method (EDIT: and there are many other reasons to use generics for dependency injection, eg supporting multiple different transport protocols)

I have this usecase where in an MQTT library I want to support TCP, TLS and QUIC. I achieve this by using generics and async traits that should be send and sync to support the most used async runtimes.

@realquantumcookie
Copy link

realquantumcookie commented Dec 28, 2022

I think the proposed approach for writing something like

fn spawn_print_all<I: AsyncIterator<next(): Send> + Send + 'static>(mut count: I)
//                                 ^^^^^^^^^^^^^^ new (syntax TBD)
where
    I::Item: Display,
{
    ...
}

would actually make sense...
Because by syntax the async keyword doesn't hint anything about Send, also if people want it to be Send by default, instead of writing the impl Future<...> + Send at the end we can also create another keyword like async send fn?; Also IMO, this way of writing, combined with trait aliases, can help lib writers a lot since we can have a more extendable trait system.

decltype approach also looks cool!

@audunhalland
Copy link
Contributor

audunhalland commented Jan 3, 2023

Idea: implied Send bound

Async Rust already has an element of implicitness: A Future which holds a non-Send type across a yield point does not implement Send. This is information that is not declared as part of async fn's signature; it's implicit.

Could this idea be "abused" and taken one step further?

trait AsyncTrait {
    async fn foo(&self);
}

async fn foo<T: AsyncTrait>(t: T) {
    tokio::spawn(async move {
        t.foo().await;
    });
}

<T as AsyncTrait>::foo's return type now must have a Send bound, because spawn requires Send. In theory this associated type bound could be automatically outputted by the compiler in an implicit way.

The downside of course is that APIs may unexpectedly break by only changing implementation without also changing the signature. But the point as mentioned is that async Rust already behaves this way. Send/non-Send async functions are indistinguishable in rustdoc.

The upsides are a smoother developer experience with reduced need for boilerplate generic bounds (given good error diagnostics), and avoid exposing new syntax.

I'm not competent enough to judge whether this would be feasible to implement in the compiler. (the real signature of a function would depend on looking inside the function, maybe the closest analogy is return_position_impl_trait?)

edit: Just after I posted I realized this would not work in dyn contexts. (but maybe there could be a dyn-specific solution to ensure future-Send-ness where the type erasure happens)

@ireina7
Copy link

ireina7 commented Jan 7, 2023

(NOT A CONTRIBUTION, JUST SUGGESTION)

It's true that we need to figure out some syntax to declare extra bounds.. But I don't think this limitation should block the whole feature.

The syntax of async is implicit per se, many details are hidden from the compiler. If users need to abstract on the returning type, they should explicitly abstract the type. To abstract on these types, TAIT and RPITIT are necessary.

Maybe in the future we can invent a more powerful syntax to constraint extra bounds on method types outside the trait. I really love it.

@hseeberger
Copy link

It's true that we need to figure out some syntax to declare extra bounds.. But I don't think this limitation should block the whole feature.

I agree. In my EventSourced library I am using "async fn in trait" in anger. I ran into the "future is not Send" issue, but it can be easily worked around as described in the blog post. I also ran into some other – probably related – issues like bogus higher-ranked lifetime error, but I also found workarounds.

On the long run we need Send and other bounds and the whole developer experience should become much easier. But nevertheless the feature is already incredibly useful if one is willing to navigate around the existing issues.

@petar-dambovaliev
Copy link
Contributor

I wanted to refactor a part of our codebase and remove duplicated associated functions by making a trait with default methods and having the structs use that. Unfortunately, i eventually hit this problem. All of the functions in question are async and are using in tokio::spawn.

@tmandry
Copy link
Member Author

tmandry commented May 15, 2023

Thanks to everyone who left feedback! It seems like the answer to the question posed by this issue is "yes". I'm going to close this.

The blog post talks about what's next: https://blog.rust-lang.org/inside-rust/2023/05/03/stabilizing-async-fn-in-trait.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-async-await Area: Async & Await AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. C-discussion Category: Discussion or questions that doesn't represent real issues. F-async_fn_in_trait Static async fn in traits T-lang Relevant to the language team, which will review and decide on the PR/issue. WG-async Working group: Async & await
Projects
None yet
Development

No branches or pull requests