I am working with tokio and I spent whole day trying to create a vector of futures.
I always got into a fight with borrow checker until finally someone suggested to use async move {}
trick.
I am failing to comprehend why this one works (unlike the naive approach).
Can somebody help me understand that please?
use futures::future;
async fn kill(processes: Vec<tokio::process::Child>) {
let mut deaths = Vec::new();
for mut p in processes {
// following works
deaths.push(async move { p.kill().await });
// naive approach would trigger error:
// deaths.push(p.kill());
// "borrowed value does not live long enough"
}
future::join_all(deaths).await;
}
Each time you write async { ... }
the Rust compiler generates code for a state machine, which we call a task. In your example, the tasks are collected into a vector, but are only actually executed after the loop is finished.
Simplifying considerably, the task that Rust generates looks something like this:
struct Task1<'a> {
p: &'a tokio::process::Child,
}
impl Future for Task1 { ... }
Without the move
keyword, these tasks hold references to p
. p
only lives for one iteration of the loop, but the task is run after the loop, so this reference is invalid - if Rust allowed you to use it, it would be a dangling pointer, triggering Undefined Behaviour.
Writing move async
means that the generated task has taken ownership of p
. It's not a reference anymore, the value is part of the task:
struct Task1 {
p: tokio::process::Child,
}
impl Future for Task1 { ... }