`async_std` and `futures` related question, need help:)

Hi everyone, I'm new to Rust and I'm porting my high-performance network service into Rust as a POC to have a try. As I need performance, then I'm very interested on the rust async.

After I went through the some videos in Rust conf and read the doc in async-std, I still quite got some questions which I hope that you guys can help me to solve or give me some idear:)

  1. I choose async-std because it wrap bascially all the std lib in async/await support. But why I still see futures crate needed in https://book.async.rs/. As I thought async-std should be a single lib I should use and I got everything. It's quite confuse me, as sometime I use async-std, and sometime I have to use future crate (like the join! and select! macro)..... Any deep explanation for this plz? :slight_smile:

  2. All the examples told me that I can use async block to create a Future instance which I can .await on it. Could some one to explain to me what big different with the followed code, as they all get the same result:

All done, future 1 result, future 2 result

Some team member ask me, honestly, I really don't know how to answer that question, I'm confusing When to use which way, plz help :slight_smile:

type FutureResult<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

///
/// `#[async_std::main]` attribute is enabled in `Cargo.toml` (features = ["attributes"] )
/// After that, we can mark the `main` function as `async`, then we can call `.await` inside the
/// function body :)
///
// Version 1
#[async_std::main]
async fn main() -> FutureResult<()> {
    let fut_1 = async { 
        task::sleep(std::time::Duration::from_secs(1)); 
        "future 1 result".to_string()
    };

    let fut_2 = async { 
        task::sleep(std::time::Duration::from_secs(1));
        "future 2 result".to_string() 
    };

    task::block_on(async {
        println!("All done, {}, {}", fut_1.await, fut_2.await);
    });

    Ok(())
}


// Version 2
#[async_std::main]
async fn main() -> FutureResult<()> {
    
    let fut_1 = task::spawn(async {
        task::sleep(std::time::Duration::from_secs(1));
        "future 1 result".to_string()
    });

    let fut_2 = task::spawn(async {
        task::sleep(std::time::Duration::from_secs(1));
        "future 2 result".to_string()
    });

    task::block_on(async {
        println!("All done, {}, {}", fut_1.await, fut_2.await);
    });

    Ok(())
}


// Version 3
#[async_std::main]
async fn main() -> FutureResult<()> {
    async fn fut_1() -> String {
        task::sleep(std::time::Duration::from_secs(1));
        "future 1 result".to_string()
    };

    async fn fut_2() -> String {
        task::sleep(std::time::Duration::from_secs(1));
        "future 2 result".to_string()
    };

    task::block_on(async {
        println!("All done, {}, {}", fut_1().await, fut_2().await);
    });

    Ok(())
}

// Version 4
#[async_std::main]
async fn main() -> FutureResult<()> {
    async fn fut_1() -> String {
        task::sleep(std::time::Duration::from_secs(1));
        "future 1 result".to_string()
    };

    async fn fut_2() -> String {
        task::sleep(std::time::Duration::from_secs(1));
        "future 2 result".to_string()
    };

    let fut_1_task = task::spawn(fut_1());
    let fut_2_task = task::spawn(fut_2());

    task::block_on(async {
        println!("All done, {}, {}", fut_1_task.await, fut_2_task.await);
    });

    Ok(())
}

Edit: as a prefix, I forgot that this was on internals.rust-lang.org, not users.rust-lang.org. In future, questions like this, which are much more about using libraries than developing the compiler or the library ecosystem, would be much better placed in users.rust-lang.org.


I think the biggest reason futures is separate from async-std is that its functionality is more general. The things futures exports can be used by async code for any async runtime, including async-std but also tokio and others. In general, having a single library that does everything is pretty uncommon in the rust ecosystem - usually, things are split up into reasonable smaller libraries. Using multiple libraries to interact with one ecosystem is common - especially when there are different competing libraries (like async-std and tokio) and some other bunch of functionality which is independent of the competition (futures).

Versions 1 and 3 are identical in behavior, and versions 2 and 4 are identical. But there's a key difference between using task::spawn and not using it: in versions 1,3, it's guaranteed that fut_1 runs all the way to completion before fut_2 is started. In versions 2,4, they can run in any order (and if fut_1 happens to complete first, that's either coincidence or an implementation detail, not guaranteed).

To make this difference more clear, I recommend making three changes. First, use .await on the task::sleep calls, so that they actually pause the futures (currently they do nothing). Second, change the timeout so that they don't wait the same amount of time. And third, print from within the futures - so you can see when they complete relative to eachother:

#[async_std::main]
async fn main() -> FutureResult<()> {
    let fut_1 = async {
        task::sleep(std::time::Duration::from_secs(2)).await;
        println!("future 1 done after 2 seconds");
        "future 1 result".to_string()
    };

    let fut_2 = async {
        task::sleep(std::time::Duration::from_secs(1)).await;
        println!("future 2 done after 1 seconds");
        "future 2 result".to_string()
    };

    task::block_on(async {
        println!("All done, {}, {}", fut_1.await, fut_2.await);
    });

    Ok(())
}
// future 1 done after 2 seconds
// future 2 done after 1 seconds
// All done, future 1 result, future 2 result

And version 2:

#[async_std::main]
async fn main() -> FutureResult<()> {

    let fut_1 = task::spawn(async {
        task::sleep(std::time::Duration::from_secs(2)).await;
        println!("future 1 done after 2 seconds");
        "future 1 result".to_string()
    });

    let fut_2 = task::spawn(async {
        task::sleep(std::time::Duration::from_secs(1)).await;
        println!("future 2 done after 1 seconds");
        "future 2 result".to_string()
    });

    task::block_on(async {
        println!("All done, {}, {}", fut_1.await, fut_2.await);
    });

    Ok(())
}
// future 2 done after 1 seconds
// future 1 done after 2 seconds
// All done, future 1 result, future 2 result

If you run these, the different output should make the difference clear.


Conceptually, using .await in version 1 is "including" each future in turn as part of the current future. It's akin to a direct function call - x.await performs all of x before continuing. In version 2, though, you give the future to async-std and ask it to run it to completion - so it'll be running regardless of whether or not you .await it. In that version, the .await is simply waiting on async-std's tasks to finish, and fut_2 will already be running by the time you .await on the task for fut_1.


As for what version you should choose: I generally recommend 1,2 over 3,4 - async blocks are more clear than inline functions. 3/4 really just move the logic into functions, and that's a reasonable thing to do - but if they're small and you just call them once, might as well not. async fn (..) { ... } is identical to fn (..) -> impl Future<...> { async { ... } }.

Both 1 and 2 are reasonable solutions, though: you should choose the one with the behavior you want. If you want these to execute in sequence, then just write async blocks and .await them. If you want them to run in parallel, then spawn them as tasks.

Hope that helps clarify things! Let me know if you have any questions about this.

5 Likes

Thanks, it's super clear explanation I've never seen, thanks a lot. :grinning: :blush:

In future, questions like this, which are much more about using libraries than developing the compiler or the library ecosystem, would be much better placed in users.rust-lang.org.

For this, please accept my apology, I will keep that in mind in the future! :slight_smile:

2 Likes