Network services in Rust are overwhelmingly async, and its poll-based model explains many of its rules and errors.
An async fn does not run when called. It returns a future: a value implementing std::future::Future, a state machine that the compiler generates from your function, with one state per .await point. Nothing happens until something polls it. `.await` inside another async context drives a future to completion, suspending the current task whenever the inner future is not ready.
The trait is small:
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}poll returns Poll::Ready(value) or Poll::Pending. When pending, the future stores the `Waker` from cx; when its I/O or timer is ready, it calls wake(), telling the executor to poll it again. No thread is blocked while waiting.
The standard library defines the trait but ships no executor. You choose a runtime (Tokio, smol, embassy for embedded) that polls tasks, plus an I/O reactor that integrates with epoll/kqueue/IOCP. The example builds a tiny executor using only std to show the mechanics.
Key consequences:
- Async is cooperative: a task gives up the thread only at
.await. A long CPU loop or a blocking call likestd::thread::sleepor blocking file I/O stalls every task on that worker thread. - Async helps when you have many concurrent waits (thousands of sockets) with little CPU work. For CPU-bound parallelism, threads or rayon are simpler.
- Futures can be cancelled by simply dropping them, which means code after an
.awaitmight never run.
Other syntax: async { ... } blocks create futures inline, async move captures by value, async closures async |x| ... are stable since 1.85, and async fn is allowed in traits since 1.75.
Example
use std::{future::Future, pin::pin, sync::Arc, thread::{self, Thread}};
use std::task::{Context, Poll, Wake, Waker};
struct ThreadWaker(Thread);
impl Wake for ThreadWaker {
fn wake(self: Arc<Self>) { self.0.unpark(); } // waking = unparking the blocked thread
}
// a minimal executor: poll; if pending, park the thread until woken
fn block_on<F: Future>(fut: F) -> F::Output {
let mut fut = pin!(fut);
let waker = Waker::from(Arc::new(ThreadWaker(thread::current())));
let mut cx = Context::from_waker(&waker);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => thread::park(),
}
}
}
async fn add(a: u32, b: u32) -> u32 { a + b }
fn main() {
let fut = async { add(1, 2).await * 10 }; // nothing runs yet: futures are lazy
println!("{}", block_on(fut)); // 30
}Remember
async fnreturns a lazy future;.awaitdrives it- Executors poll futures; wakers tell them when to poll again
- Never block inside async code: tasks only yield at
.await
Watch out for
- Calling an async function without
.awaitand wondering why nothing happened (the compiler warns: futures do nothing unless awaited).