Rust Async Await
Many programs spend most of their time waiting: for a web server reply, a file read, or a database answer. Threads handle waiting by sleeping, yet thousands of sleeping threads consume large amounts of memory. Asynchronous programming offers a lighter approach. Think of a restaurant waiter who takes an order, moves to the next table while the kitchen cooks, and returns when the food is ready.
Blocking and Non-Blocking Work
BLOCKING (one waiter per table) Waiter 1: take order ... wait ... serve Waiter 2: take order ... wait ... serve ASYNC (one waiter, many tables) Waiter: order A -> order B -> serve A -> order C -> serve B ...
Async code lets one thread juggle many waiting tasks.
The async Keyword
Placing async before a function turns it into a function that returns a future. A future describes work that will finish later. Calling the function does not run the body.
async fn greet() -> String {
String::from("Hello from async")
}The return type is no longer String. The call produces a value that promises a String eventually.
The await Keyword
The .await keyword pauses the current task until a future completes. While the task waits, the runtime switches to other ready tasks.
async fn show() {
let text = greet().await;
println!("{text}");
}The keyword works only inside async functions and blocks.
Futures Are Lazy
A future does nothing until something polls it. Creating a future without awaiting it wastes the effort, because the work never starts. Treat a future like a recipe card: reading the card does not cook the meal.
Why a Runtime Is Needed
The standard library defines futures yet ships no executor, the engine that drives futures to completion. Programs choose a runtime crate. The most common choice is tokio.
[dependencies]
tokio = { version = "1", features = ["full"] }A Working Program
The attribute #[tokio::main] sets up the runtime and lets main be async.
use tokio::time::{sleep, Duration};
async fn fetch_data(id: u32) -> String {
sleep(Duration::from_millis(500)).await;
format!("data {id}")
}
#[tokio::main]
async fn main() {
let result = fetch_data(1).await;
println!("{result}");
}
data 1
Running Tasks Together
Awaiting tasks one after another still runs them in sequence. The tokio::join! macro starts several futures at once and waits for all of them.
#[tokio::main]
async fn main() {
let (a, b, c) = tokio::join!(
fetch_data(1),
fetch_data(2),
fetch_data(3),
);
println!("{a}, {b}, {c}");
}
Sequential: |--1--|--2--|--3--| 1.5 seconds
Joined: |--1--|
|--2--|
|--3--| 0.5 seconds
Spawning Background Tasks
The tokio::spawn function launches a task that runs independently. The returned handle lets you collect the result later.
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
fetch_data(7).await
});
let output = handle.await.unwrap();
println!("{output}");
}Async Compared with Threads
| Feature | Threads | Async Tasks |
|---|---|---|
| Memory per unit | Large | Very small |
| Best for | Heavy calculation | Waiting on input and output |
| Switching | Operating system decides | Tasks yield at await points |
| Typical count | Dozens | Thousands or more |
Common Pitfalls
- Calling a slow blocking function inside an async task freezes other tasks. Use async versions such as
tokio::time::sleepinstead ofstd::thread::sleep. - Forgetting
.awaitleaves the work unstarted, and the compiler emits a warning. - Holding a standard
Mutexguard across an await point can block the runtime. Usetokio::sync::Mutexin that case.
Where Async Shines
- Web servers handling many connections
- Programs that call several network services
- Chat applications and live data feeds
- Tools that read many files or sockets concurrently
Heavy math on large data fits threads better, because async tasks yield only at await points.
Practice Ideas
- Write three async functions that sleep for different times and run them together with
join!. - Time sequential awaits against joined awaits using
std::time::Instant. - Spawn five tasks and collect their return values in a vector.
Key Points
async fnreturns a future, and.awaitwaits for its result.- Futures stay idle until a runtime drives them.
tokiosupplies a popular runtime and helper tools.- Use
join!orspawnto run tasks together. - Avoid blocking calls inside async code.
