Your Rust async
code is a “liar”—and I’m going to strip its state machine down to the bones.
Have you ever written an async
function, happily called it, and then… the program just ended with nothing happening?
You stare at the screen: Where did my code go? Where’s my println!
? Did I just run emptiness?
Don’t panic. You’re not alone. Welcome to the world of Rust async
, where the first rule is: what you see isn’t necessarily what’s happening.
async/await
is Rust’s concurrency ace. It lets you write non-blocking, high-performance programs with code that looks synchronous, handling thousands of connections with ease. But behind the elegant sugar lies a precise mechanism that confuses many newcomers.
Today, we’ll go into Rust’s engine room and make Future
, .poll()
, and the state machine crystal clear.
Act 1: The big misunderstanding — async fn
does not run immediately
Start with the most basic “trick”. You write:
async fn say_hello() {
println!("Hello, from the future!");
}
Then you call it:
fn main() {
say_hello();
// Program exits; nothing is printed
}
Why? Because calling an async
function does not execute its body. It returns a value called a Future
.
What is a Future
?
Think of it as a “voucher” redeemable in the future.
The voucher itself does nothing. Holding it won’t magically produce a prize. say_hello()
returns such a voucher that says: “I promise to print a line at some point in the future.”
Your main
gets the voucher and throws it away. The program ends.
Remember: an async
function returns a plan, not a result.
Act 2: Ignition — the executor and .await
How do we redeem the voucher?
You need an executor, like tokio
or async-std
.
The executor is like a kitchen manager with infinite energy.
Your Future
(the voucher) is a set of recipes.
When you hand recipes to the manager, work actually begins. In code, this handoff is often handled by the #[tokio::main]
macro or a manual block_on
.
#[tokio::main]
async fn main() {
say_hello().await; // This time, it prints!
}
Notice the magical .await
. It’s the ignition switch.
What does .await
do? It tells the kitchen manager (executor):
“Hey, I’m starting
say_hello
. If it needs time (like preheating an oven), don’t just wait there. Go do other things. When I’m ready, I’ll notify you.”
This is the core of async/await
: non-blocking. .await
is the point where a task can pause and yield control.
Act 3: Under the hood — Future
is a state machine
Now the final reveal. When the compiler sees your async fn
, it secretly transforms it into a struct that implements the Future
trait.
That struct is a small state machine.
Consider a slightly more complex example:
async fn cook_meal() {
println!("Start washing vegetables...");
let ingredients = chop_vegetables().await; // first suspension point
println!("Chopping done, start frying...");
let dish = fry_in_pan(ingredients).await; // second suspension point
println!("Serve!");
}
The compiler turns it into a state machine roughly like this:
- State 0: initial
- State 1: chopping (waiting for
chop_vegetables
) - State 2: frying (waiting for
fry_in_pan
) - State 3: done
The executor repeatedly asks the state machine one question via .poll()
: “Are you done yet?”
.poll()
can return:
Poll::Ready(result)
: Done! Here’s the result.Poll::Pending
: Not ready. I’m waiting for something (network, timer). Go do other work. I’ll use aWaker
to notify you when to come back.
The flow looks like this:
- Manager polls
cook_meal
. - Code runs
println!("Start washing vegetables...")
. - Hits
chop_vegetables().await
, manager starts polling the chopping task. - Chopping takes time → returns
Pending
. Manager shelvescook_meal
and handles other recipes. - Later, an external event (I/O, timer) fires and uses the
Waker
to wakecook_meal
. - Manager polls again; it resumes from where it paused and gets the chopped ingredients.
- Hits
fry_in_pan().await
; repeat. - Eventually,
.poll()
returnsReady
; task completes.
No magic. Just a precise state machine driven by poll
and Waker
. Your async
code runs in small pieces, cooperatively scheduled.
Recap: Putting the elephant in the fridge
Concept | Plain analogy | Purpose |
---|---|---|
async fn | a recipe | Defines a task to run later; doesn’t run itself. |
Future | voucher + progress tracker | The state machine that records where execution is. |
Executor (e.g., Tokio) | tireless kitchen manager | Schedules/advances many futures to keep the kitchen busy. |
.await | “wait, but don’t idle” | Suspension point that yields control to the executor. |
.poll() | “Done yet?” | Advances the state machine one step. |
Waker | timer/alarm | Notifies the executor to poll again when ready. |
Next time your async
code behaves counterintuitively, remember: it’s not lying. It’s faithfully following the kitchen workflow. Your job is to write good recipes and place .await
at the right spots.
Follow Rexai Programming on WeChat to unlock more dev magic.