Do we need Send bounds to stabilize async_fn_in_trait? · Issue #103854 · rust-lang/rust (original) (raw)

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) -> OptionSelf::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

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

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 = OptionSelf::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

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

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.