Async runtime and futures
Get started with async Rust and Tokio: understand futures, async/await, ownership, and runtime setup with small code examples.
Start here: Rust + Tokio#
Tokio schedules async Rust tasks and provides timers, networking, and I/O.
Create a binary crate with dependencies in Cargo.toml and an entry point in src/main.rs. The full feature enables the APIs used in these examples; enable individual features for a smaller build.
cargo new tokio-guide
cd tokio-guide
cargo run[package]
name = "tokio-guide"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }use std::time::Duration;
#[tokio::main]
async fn main() {
println!("starting");
tokio::time::sleep(Duration::from_millis(200)).await;
println!("finished");
}#[tokio::main] creates a runtime, enters it, and runs main. The sleep yields control instead of blocking a worker thread.Rust basics for async code#
Ownership, borrowing, moves, Result, and Option are the main Rust concepts used in async code.
A value normally has one owner. Passing it into async move transfers ownership into that future. Use clone() to create an independent owned value, or Arc::clone() to share one allocation safely between tasks. A borrow (&T or &mut T) temporarily lends access; a spawned task usually must own its data because it may outlive the current function.
fn length(name: &str) -> usize {
name.len() // borrows; does not consume name
}
fn main() {
let name = String::from("Ada");
println!("{}: {}", name, length(&name));
let moved = name; // moved is now the owner
println!("{moved}");
// println!("{name}"); // error: value moved
}fn parse_port(raw: &str) -> Result<u16, std::num::ParseIntError> {
raw.parse::<u16>()
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let port = parse_port("8080")?; // return early on Err
let maybe_name: Option<&str> = Some("web");
println!("{} on {}", maybe_name.unwrap_or("app"), port);
Ok(())
}tokio::spawn needs owned data and normally Send + 'static. The latter means it contains no short-lived borrowed references; it does not mean the task lives forever.Futures, async, and .await#
An async function returns a Future. A future describes work; the runtime polls it when it can make progress.
async fn returns immediately with a future. Nothing inside its body runs until the future is polled. .await polls it, and if the work is not ready, suspends this task so other tasks can run. Only an async context can use .await. A suspended future keeps its local state and resumes later.
use std::time::Duration;
async fn fetch(label: &str) -> String {
tokio::time::sleep(Duration::from_millis(100)).await;
label.to_owned()
}
#[tokio::main]
async fn main() {
let a = fetch("A").await;
let b = fetch("B").await; // starts after A finishes
let (c, d) = tokio::join!(fetch("C"), fetch("D"));
// C and D make progress together in this task.
println!("{a} {b} {c} {d}");
}join! drives multiple futures in one task, so they can overlap while waiting. spawn creates independent tasks that may also run on multiple worker threads. A future that blocks a thread prevents that worker from doing other work.Runtime and entry points#
The runtime schedules tasks and provides time, I/O, and networking drivers. Most applications start with #[tokio::main].
#[tokio::main]
async fn main() {
println!("running in Tokio");
}#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("one scheduler thread");
}fn main() -> Result<(), Box<dyn std::error::Error>> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all() // I/O and time drivers
.build()?;
runtime.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
});
Ok(())
}block_on from inside an async Tokio task. At synchronous boundaries, use a runtime once; inside async code, just .await.Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice