Async Runtime
When your program asks for something that takes time, e.g reading a file, fetching data from a server, waiting for a timer, it has two choices: sit and wait doing nothing, or go do something else while it waits.
Blocking means sitting and waiting. Your program freezes until the result comes back.
Nothing else can happen in the meantime.
Non-blocking (async) means your program starts the task, steps away, and comes back to collect
the result when it is ready. While it waits, it can work on other things.
In Rust, async code is written using two keywords:
asyncmarks a function as non-blocking. Calling it does not run it immediately. It gives you back a task that is ready to be run..awaitis where you hand control back and say “run this task, and come back to me when it is done.”
Before your non-blocking code can run, something needs to manage it, deciding which tasks run, when they run, and what to do while they are waiting. That manager is called a runtime.
Think of it like a coordinator at a call center; instead of one person sitting idle waiting for a call back, the coordinator keeps everyone busy by assigning new work while others wait.
Tokio is that runtime for Rust. Without it, async functions and .await do nothing on their own.
Macro
The easiest way to start the Tokio runtime is with the tokio::main macro. Put it above your
main function and it handles everything for you, starting the runtime, running your code, and
shutting it down when done. It also lets main be non-blocking, so you can use .await directly
inside it.
async fn fetch_network_request() -> u32 {
89
}
#[tokio::main]
async fn main() {
let result = fetch_network_request().await;
assert_eq!(result, 89);
}
Add tokio to Cargo.toml with the macros and rt-multi-thread features enabled.
[dependencies]
tokio = { version = "*", features = ["macros", "rt-multi-thread"] }
Builder Approach
tokio::main works well for most programs, but it makes all the decisions for you.
If you need control over how the runtime is set up, use the Builder instead.
Think of Builder as a recipe. Each method you call adds an instruction, how many threads to
use, what to call them, how much memory to give each one. Nothing actually happens until
you call .build() at the end, which is when the runtime is created and ready to run.
In this example, the recipe sets up 4 worker threads, gives them the name “thread-one”, and sets a stack size of 3MB each.
use std::io;
use tokio::runtime::Builder;
async fn fetch_network_request() -> u32 {
89
}
fn main() -> io::Result<()> {
let runtime = Builder::new_multi_thread()
.worker_threads(4)
.thread_name("thread-one")
.thread_stack_size(3 * 1024 * 1024)
.build()?;
runtime.spawn(async {
let result = fetch_network_request().await;
assert_eq!(result, 89);
});
Ok(())
}
Add tokio to Cargo.toml with the rt-multi-thread feature enabled.
[dependencies]
tokio = { version = "*", features = ["rt-multi-thread"] }