Search code examples
rustrust-tokiorust-async-std

How do i run a future without awaiting it? (in rust)


I have some async function

async fn get_player(name: String, i: Instant) -> Option<Player> {
// some code here that returns a player structs
}

in my main function i want to run the above function concurrently in a loop, this function takes about 1sec to complete and I need to run it at least 50 times, hence I would like to make it concurrently run this function 50 times. in my main function i have a lazy_static custom Client struct which should not be created more than once.

main function

#[tokio::main]
async fn main() {
    client.init().await;

    println!("start");
    for i in 0..10 {
        println!("{}", i);
        let now = Instant::now();

        tokio::spawn(async move  {
            client.get_player("jay".to_string(), now).await;
        });
    }
    loop {}
}

the reason I am passing instant is because in my get_player function i have a println!() that just prints the execution time.

the above main method takes around 500ms for each function call, however the below code only takes 100ms.

#[tokio::main]
async fn maain(){
    client.init().await;

    for i in 0..10 {
        let now = Instant::now();
        client.get_player("jay".to_string(), now).await.expect("panic");
    }
}

but this function is still synchronous code, how do I actually run the async function concurrently without the time cost?

  • To better understand what am after is an implemention similar to this (its in java btw),
     CompleteableFuture.thenAccept(x -> x.SayHello(););

or in Js its something like .then after an async function.

is there any similar implementation in rust?


Solution

  • I assume that your get_player function takes one second because it waits for a network interaction, and not because some computation takes that long. If it's compute-bound instead, asynchronism is the wrong approach and you want to go with parallelism instead.

    Further, I assume that the function signature of get_player is actually async fn get_player(&self, name: String, i: Instant) -> Option<Player> instead, because otherwise none of your main code samples would make any sense. Although I'm confused why it would be &self and not &mut self.

    With those assumptions, I tried to reproduce your minimal reproducible example:

    use std::time::{Duration, Instant};
    
    #[derive(Debug)]
    struct Player {
        name: String,
    }
    
    struct Client {}
    
    impl Client {
        async fn init(&self) {}
    
        async fn get_player(&self, name: String, _now: Instant) -> Option<Player> {
            // Dummy code that simulates a delay of 1 second
            tokio::time::sleep(Duration::from_millis(1000)).await;
            Some(Player { name })
        }
    }
    
    static client: Client = Client {};
    
    #[tokio::main]
    async fn main() {
        let begin = Instant::now();
        client.init().await;
    
        for i in 0..10 {
            let now = Instant::now();
            let player = client
                .get_player(format!("Player #{}", i), now)
                .await
                .expect("panic");
            println!(
                "[{} ms] Retreived player: {:?}",
                begin.elapsed().as_millis(),
                player.name
            );
        }
    }
    
    [1002 ms] Retreived player: "Player #0"
    [2004 ms] Retreived player: "Player #1"
    [3005 ms] Retreived player: "Player #2"
    [4008 ms] Retreived player: "Player #3"
    [5010 ms] Retreived player: "Player #4"
    [6011 ms] Retreived player: "Player #5"
    [7013 ms] Retreived player: "Player #6"
    [8014 ms] Retreived player: "Player #7"
    [9016 ms] Retreived player: "Player #8"
    [10018 ms] Retreived player: "Player #9"
    

    This is based on your last main example. As you can see, it takes 10 seconds to retrieve all players, because they all run in sequence.

    Now let's run them all asynchronously. The problem here is joining them all simultaneously. Tokio sadly doesn't offer an easy way for that; you could tokio::spawn all of them, collect the JoinHandles and then join them one by one. The crate futures, however, offers exactly what you want:

    use std::time::{Duration, Instant};
    
    #[derive(Debug)]
    struct Player {
        name: String,
    }
    
    struct Client {}
    
    impl Client {
        async fn init(&self) {}
    
        async fn get_player(&self, name: String, _now: Instant) -> Option<Player> {
            // Dummy code her that simulates a delay of 1 second
            tokio::time::sleep(Duration::from_millis(1000)).await;
            Some(Player { name })
        }
    }
    
    static client: Client = Client {};
    
    #[tokio::main]
    async fn main() {
        let begin = Instant::now();
        client.init().await;
    
        let get_player_futures = (0..10).into_iter().map(|i| async move {
            let now = Instant::now();
            let player = client
                .get_player(format!("Player #{}", i), now)
                .await
                .expect("panic");
            println!(
                "[{} ms] Retreived player: {:?}",
                begin.elapsed().as_millis(),
                player.name
            );
        });
    
        futures::future::join_all(get_player_futures).await;
    }
    
    [1002 ms] Retreived player: "Player #0"
    [1002 ms] Retreived player: "Player #1"
    [1002 ms] Retreived player: "Player #2"
    [1002 ms] Retreived player: "Player #3"
    [1002 ms] Retreived player: "Player #4"
    [1002 ms] Retreived player: "Player #5"
    [1002 ms] Retreived player: "Player #6"
    [1002 ms] Retreived player: "Player #7"
    [1003 ms] Retreived player: "Player #8"
    [1003 ms] Retreived player: "Player #9"
    

    As you can see, the entire program only took one second, and all of them got retrieved simultaneously.

    get_player_futures here is an iterator over all the futures that need to be awaited for in order to retrieve the players. futures::future::join_all then awaits all of them simultaneously. You can even use join_all's return value to retrieve the values of the futures, but we don't use that here.

    I hope that helped somehow; it was hard to create an answer as parts of your question were incoherent.