When you use .await
in an async function, the compiler builds a state machine behind the scenes. Each .await
introduces a new state (while it waits for something) and the code in between are state transitions (aka tasks), which will be triggered based on some external event (e.g. from IO or a timer etc).
Each task gets scheduled to be executed by the async runtime, which could choose to use a different thread from the previous task. If the state transition is not safe to be sent between threads then the resulting Future
is also not Send
so that you get a compilation error if you try to execute it in a multi-threaded runtime.
It is completely OK for a Future
not to be Send
, it just means you can only execute it in a single-threaded runtime.
Perhaps someone could shed some light on this with an example of why having a non-Send
type in a Future
is ok, but holding it across an await
is not?
Consider the following simple example:
async fn add_votes(current: Rc<Cell<i32>>, post: Url) {
let new_votes = get_votes(&post).await;
*current += new_votes;
}
The compiler will construct a state machine like this (simplified):
enum AddVotes {
Initial {
current: Rc<Cell<i32>>,
post: Url,
},
WaitingForGetVotes {
current: Rc<Cell<i32>>,
fut: GetVotesFut,
},
}
impl AddVotes {
fn new(current: Rc<Cell<i32>>, post: Url) {
AddVotes::Initial { current, post }
}
fn poll(&mut self) -> Poll {
match self {
AddVotes::Initial(state) => {
let fut = get_votes(&state.post);
*self = AddVotes::WaitingForGetVotes {
current: state.current,
fut
}
Poll::Pending
}
AddVotes::WaitingForGetVotes(state) => {
if let Poll::Ready(votes) = state.fut.poll() {
*state.current += votes;
Poll::Ready(())
} else {
Poll::Pending
}
}
}
}
}
In a multithreaded runtime, each call to poll
could be from a different thread, in which case the runtime would move the AddVotes
to the other thread before calling poll
on it. This won't work because Rc
cannot be sent between threads.
However, if the future just used an Rc
within the same state transition, it would be fine, e.g. if votes
was just an i32
:
async fn add_votes(current: i32, post: Url) -> i32 {
let new_votes = get_votes(&post).await;
// use an Rc for some reason:
let rc = Rc::new(1);
println!("rc value: {:?}", rc);
current + new_votes
}
In which case, the state machine would look like this:
enum AddVotes {
Initial {
current: i32,
post: Url,
},
WaitingForGetVotes {
current: i32,
fut: GetVotesFut,
},
}
The Rc
isn't captured in the state machine because it is created and dropped within the state transition (task), so the whole state machine (aka Future
) is still Send
.
await
needs to be stored in the future, since the function returns at this point, storing its state. And if you store something that's notSend
in a future, the future itself can't beSend
. – IndoiranianSend
as an argument to anasync fn
, which has being called to generate theFuture
but has not yet been polled. I guess under the hood these arguments are also stored inside the generatedFuture
? – Geologyasync fn
uses it. If theasync fn
consumes it beforeawait
-ing then the future would not need to store it, and should thus be Send. – Rawhideawait
, I think the future will still need to store them. When the async fn is called, it returns the future immediately, and AFAIK the body of the function doesn't start executing untilpoll()
is invoked on the future. – Mannaasync fn foo()
andfn bar() -> impl Future
:bar
can do work immediately upon being called, whereasfoo
has to store all its arguments and wait to be polled. – Kauffmannasync
functions and functions with anasync
block, probably. – Rawhide