async_await
In Rust, async does NOT mean “run this on another thread.”
This is the single most important thing to understand.
Instead:
asyncmeans “this function can be paused and resumed without blocking the thread.”
Rust async is about cooperative multitasking, not preemptive multitasking.
Blocking vs async (mental model)
- Blocking code:
let data = socket.read(); // thread is stuck here - Async code:
let data = socket.read().await; // thread can do other work
When you await, you’re saying:
“If this isn’t ready yet, pause me and let something else run.”
async fn — what it really returns
When you write:
async fn fetch() -> u32 {
42
}
You might think it returns a u32.
It does not.
It actually returns:
fn fetch() -> impl Future<Output = u32>
So this:
let x = fetch();
does not run the function
it just creates a Future
Nothing executes until the future is polled (usually by .await).
What is a Future
A Future is basically a state machine.
Simplified definition:
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
Poll has two states:
enum Poll<T> {
Ready(T),
Pending,
}
Ready(value)→ computation finishedPending→ not ready yet, try again later
Rust async is pull-based:
- An executor keeps polling futures
- Futures say “not ready” or “done”
What await actually does
When you write:
let result = some_future.await;
The compiler rewrites this into:
- A state machine
- That yields control when the future returns
Pending - And resumes from the same spot later
Key rule
.awaitcan only appear inside anasynccontext
Because only async functions can be turned into resumable state machines.
Async does not do anything by itself
This code does nothing:
async fn hello() {
println!("hello");
}
fn main() {
hello(); // nothing happens
}
Why? Because:
- No executor
- Future is never polled
You need:
- An async runtime (Tokio, async-std, smol, etc.)
Minimal working async example (Tokio)
Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
Example 1: Basic async function
use tokio::time::{sleep, Duration};
async fn say_hello() {
println!("hello...");
sleep(Duration::from_secs(1)).await;
println!("world!");
}
#[tokio::main]
async fn main() {
say_hello().await;
}
say_hello()returns aFuture.awaitpolls itsleep()returnsPending- Tokio pauses
say_hello - Timer completes
- Tokio resumes
say_hello "world!"prints
The thread was free during the sleep
Async concurrency (not parallelism)
Running multiple async tasks
use tokio::time::{sleep, Duration};
async fn task(id: u32) {
println!("task {} started", id);
sleep(Duration::from_secs(1)).await;
println!("task {} finished", id);
}
#[tokio::main]
async fn main() {
let t1 = task(1);
let t2 = task(2);
tokio::join!(t1, t2);
}
Output (order may vary)
task 1 started
task 2 started
task 1 finished
task 2 finished
- Both tasks run on one thread
- They interleave at
.await - No threads are blocked
Spawning tasks (true async scheduling)
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
println!("task A");
});
tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
println!("task B");
});
sleep(Duration::from_secs(2)).await;
}
spawnschedules independent tasks- Executor decides when to poll them
- Still async, still cooperative
Common misconceptions (very important)
- Async = multithreading: Nope.
- Async = faster: Only for IO-bound workloads.
awaitblocks: It yields, not blocks.- Async works without runtime: You always need an executor.
- When should you use async in Rust?
Use async when:
- Network IO
- File IO
- Timers
- High concurrency with low CPU usage
Avoid async when:
- Heavy CPU computation
- Tight loops
- Simple synchronous programs
(You can mix async + threads when needed.)