Search code examples
rustasync-awaitfuturerust-tokio

What is the magic of async move {}?


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;
}

Solution

  • 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 { ... }