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.
Problem: Spawning from generics
Given an ordinary trait with
async fn:It is not currently possible to write a function like this:
playground
Speaking more generally, it is impossible to write a function that
async fnof the trait from the spawned taskThe problem is that the compiler does not know the concrete type of the future returned by
next, or whether that future isSend.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.
playground
This works because spawn occurs in a context where
CountdownCountdown::nextisSendprint_all::<Countdown>and passed tospawnisSendMaking 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:
playground
Here we've added the
Sendbound by using return_position_impl_trait_in_trait syntax. We've also addedSelf: Send + 'staticfor 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
SpawnAsyncIteratortoAsyncIterator(playground)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
Further discussion about the syntax or shape of this solution should happen on the async-fundamentals-initiative repo, or a future RFC.
Questions
async_fn_in_traituntil we have a solution?If you've had a chance to give
async_fn_in_traita spin, or can relay other relevant first-hand knowledge, please comment below with your experience.