Async Introduction
Async/Await and Futures
Section titled “Async/Await and Futures”Rust’s async/.await syntax lets you write non-blocking code that looks like ordinary sequential code. Under the hood, an async fn returns a Future — a value that represents a computation that may not have completed yet.
use tokio::time::{sleep, Duration};
async fn fetch_data(id: u32) -> String { sleep(Duration::from_millis(10)).await; format!("data for id={}", id)}
#[tokio::main]async fn main() { let result = fetch_data(1).await; println!("{}", result);}The .await keyword suspends the current async function until the Future it is applied to resolves. While suspended, the executor (tokio) can run other tasks.
What Is a Future?
Section titled “What Is a Future?”A Future<Output = T> is a trait that represents a value of type T that will be available at some point in the future. Calling an async fn does not execute the body immediately — it returns a Future. The body only runs when you .await it (or poll it via an executor).
// This does nothing yet — just creates a Futurelet fut = fetch_data(42);
// This runs the body and blocks until completelet result = fut.await;This lazy evaluation is why Rust async is zero-cost: futures that are never awaited never run.
Running Tasks Concurrently with tokio::join!
Section titled “Running Tasks Concurrently with tokio::join!”To run multiple async tasks at the same time, use tokio::join!. It drives all futures concurrently on the same thread (or across worker threads, depending on the runtime configuration).
use tokio::time::{sleep, Duration};
async fn task(name: &str, ms: u64) -> String { sleep(Duration::from_millis(ms)).await; format!("{} done", name)}
#[tokio::main]async fn main() { let (a, b, c) = tokio::join!( task("alpha", 30), task("beta", 20), task("gamma", 10), ); println!("{}", a); println!("{}", b); println!("{}", c);}All three tasks start immediately. tokio::join! waits until all three complete and returns their results in order.
Spawning Async Tasks
Section titled “Spawning Async Tasks”tokio::spawn launches a task onto the tokio thread pool. It returns a JoinHandle similar to std::thread::spawn.
use tokio::task;
#[tokio::main]async fn main() { let handle = task::spawn(async { // runs concurrently on the tokio thread pool 42_u32 });
let result = handle.await.unwrap(); println!("Spawned task returned: {}", result);}async vs Threads: When to Use Which
Section titled “async vs Threads: When to Use Which”| Criterion | OS Threads (std::thread) | Async (tokio) |
|---|---|---|
| CPU-bound work | Preferred | Use spawn_blocking |
| I/O-bound work | Wasteful (blocked thread) | Preferred |
| Number of concurrent tasks | Hundreds (OS limit) | Millions (lightweight) |
| Stack size | ~8 MB per thread | ~few KB per task |
| External crate needed | No | Yes (tokio) |
Adding tokio to Your Project
Section titled “Adding tokio to Your Project”cargo add tokio --features fullOr in Cargo.toml:
[dependencies]tokio = { version = "1", features = ["full"] }Then annotate your entry point:
#[tokio::main]async fn main() { // your async code here}#[tokio::main] is a procedural macro that wraps your async fn main in a call to tokio::runtime::Runtime::block_on, starting the tokio event loop.