Async code: Tokio::main vs futures::executor::block_on vs runtime::main?

Hi,

I'm porting a codebase from sync to async Rust but I'm a little bit lost because of the lack of official documentation.

What is the right way to have an async program today?

#[runtime::main]
async fn main() {
    hello_world().await;
}

or

#[tokio::main]
async fn main() {
    hello_world().await;
}

or

use futures::executor::block_on;

async fn async_main() -> () {
    hello_world().await;
}

async fn main() {
    block_on(async_main());
}

Please note I'm using Rust nightly 1.38

Best regards

There's no 'right way', just different ways.

  • Your first example uses the runtime crate to execute the futures. runtime allows your program to abstract over multiple different executors (e.g. tokio and runtime-native), but currently doesn't support some things (e.g. asynchronous filesystem IO).
  • Your second example uses the tokio crate's executor directly. This is probably the most stable/developed executor out there at the minute.
  • Your third example uses the LocalPool executor from the futures crate. This is simple, but single-threaded, so might not be well suited to certain tasks.

I would recommend using either tokio or runtime.

4 Likes

Ack thank you!

And I didn't follow the entire story, but maybe you know:

Will the standard library have an executor or will we need to always rely on third party crates like tokio or runtime?

Will the standard library have an executor or will we need to always rely on third party crates like tokio or runtime ?

I can't see anything of the scale of tokio moving into std any time soon, as the API design space isn't fully mapped out yet (for example, tokio and futures both have their own AsyncRead and AsyncWrite traits that are slightly different!). Moving into std would effectively mean committing to never making any more breaking changes to the API, so it's not something that's taken lightly!

I could maybe see some of the simpler executors from futures ending up in std one day though, just so it's possible to make some example code work without any dependencies.

But who knows? Not me :stuck_out_tongue:

1 Like

If you need OS related stuff, use tokio::main, otherwise you can go away with executor.

tokio::main basically set ups executor ( futures::executor::block_on), IO reactor(to drive IO ops) and tokio's timer facilities.
runtime does the same but with unnecessary abstractions

Will the standard library have an executor or will we need to always rely on third party crates like tokio or runtime?

No, it has nothing to do in standard library as this is a very complex thing.
Executor alone will not help you much as you also need to drive IO

1 Like

You can also try using the async-std crate: https://async.rs/blog/announcing-async-std/
The crate was specifically designed to make transitions from synchronous code as easy as possible.

With it, your example would be written like this:

use async_std::task;

fn main() {
    task::block_on(hello_world())
}
3 Likes

For those of us that want to use async libraries on targets that Tokio doesn't support those abstractions are very necessary (I have doubts whether runtime is the right abstraction, but it's heartening to see experiments into providing these abstractions early, after a relative lack of them for futures 0.1).

1 Like

afaik runtime brings nothing platform wise, because it only abstracts tokio and it's mini fork romio

This isn't correct - runtime currently supports two backends:

  • runtime-native, which uses a concurrent scheduler
  • tokio, which uses a work-stealing scheduler.

I'm talking about IO driver (reactor) and in both cases it uses mio

I can easily write my own custom target-specific implementation of runtime-raw that doesn't use mio and suddenly all libraries that abstract out the platform via runtime will just work. I don't even need to publicly publish that implementation if it's for a very specific device that is not generally useful/covered by commercial agreements.