So I recently became aware of something that I had not considered regarding async fn
– in particular, the current default where we don’t require the resulting future to be Send doesn’t work out so well once we scale up to async fn in traits. (I take it many people are aware of this for some time, but I at least hadn’t thought about it before and I’d like to talk it out.)
The problem
The current async fn
definition returns an impl Future
– it does not promise that the resulting future is send. In practice, this is fine, because auto traits will automatically figure out whether it should be send or not. Very ergonomic, very nice:
async fn process() { }
fn foo() {
spawn(process()); // requires a sendable future; works b/c auto-traits
}
However, there is a wrinkle. When we support async fn in traits, things won’t work out as well:
trait Process {
async fn process(&self);
}
fn foo<P: Process>(p: P) {
let x = spawn(p.process()); // ERROR
}
The problem here is that the definition of foo
must be valid for all processes Process
types, including those whose resulting future is not send.
Options
If we stick with the current design, we can resolve this in three ways (detailed below):
- Some form of bound that
foo
can use to specify thatP::process
returns aSend
future - Some way to opt-in to sendability
async(Send) fn process()
- A different default in traits (e.g., async fns in traits are send by default)
Alternatively, if we change the design we could:
- default to sendable futures, but offer an opt-out like
async(?Send)
.
None of these options look that appealing to me right now. They all seem to impose a cost somewhere.
Questions
I have a few questions I would like feedback on:
- How often will people want to have single-thread executors that make use of non-Send data?
- Am I correct that anyone building on tokio, or using a framework like tide, will basically want all futures to be sendable?
- Anything missing from my summary above? Any other options we may be overlooking?
It seems to me that the problem here is that for any given project you may care or not care about sendability, but you care kind of “uniformly”. i.e., it may be that only 15% of people using futures want a single-threaded executor, but for those that do they basically always want non-sendable futures, so if we have an opt-out like async(?Send)
, they’d have to write it all the time.
Details about the options
Details about the options
Bound. Either the foo
bound needs some augmented form to specify that each of the future returned by p.process()
is Send. We don’t have a clear syntax for naming that type right now, so I’ll write P::process
:
fn foo<P: Process>(p: P)
where P::process: Send // someway to specify that the async result is send
{
let x = spawn(p.process()); // ERROR
}
Note that P: Send
is neither correct nor sufficient – we care about the future that gets returned from p.process()
, not p
itself. (And process
, in my example, is capturing a &P
, so P
would actually have to be Sync
.)
Opt-in. Or we could have some sort of opt-in:
trait Process {
async(Send) fn process(&self);
}
But it seems like quite possibly like the thing that people will want almost all the time.
Opt-out. We could say all async fn
requires that futures are sendable. But in that case we would have async(?Send) fn
for things that are not required to be sendable. Writing that a lot seems like an ergonomic hit.
Or we could have inconsistent defaults, though I think this would be really bad.