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}");
}
});
}
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! }
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
isSend
- We therefore know that the future returned by our call to
print_all::<Countdown>
and passed tospawn
isSend
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 + '_; }
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
toAsyncIterator
(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.