Skip to content

Do we need Send bounds to stabilize async_fn_in_trait? #103854

Closed
@tmandry

Description

@tmandry

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-async-awaitArea: Async & AwaitAsyncAwait-TriagedAsync-await issues that have been triaged during a working group meeting.C-discussionCategory: Discussion or questions that doesn't represent real issues.F-async_fn_in_traitStatic async fn in traitsT-langRelevant to the language teamWG-asyncWorking group: Async & await

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions