Asynchronous Rust
Synchronous vs. Asynchronous Programming
Synchronous programming binds a task to a dedicated operating-system thread from start to finish: whenever the task waits on an external event (such as a network socket read, disk I/O, or timer), its OS thread sits blocked in the kernel scheduler. Asynchronous programming decouples tasks from OS threads: when a task reaches a waiting point (.await), it yields control back to an executor on the current thread so the same OS thread can immediately advance hundreds or thousands of other ready tasks.
Execution Timeline Comparison
Synchronous (1 OS Thread per Task — Thread Stalls During I/O Wait):
Thread 1: [CPU][========= Waiting on Socket 1 =========][CPU]
Thread 2: [CPU][========= Waiting on Socket 2 =========][CPU]
Thread 3: [CPU][========= Waiting on Socket 3 =========][CPU]
(3 OS threads, 3 x 2 MiB stacks = 6 MiB RAM, 6 kernel context switches)
Asynchronous (N Tasks Multiplexed on 1 OS Thread):
Thread 1: [T1 CPU][T2 CPU][T3 CPU] ... (OS epoll/kqueue waits) ... [T1 CPU][T2 CPU][T3 CPU]
(1 OS thread, 3 tiny ~128-byte Future structs, 0 kernel thread context switches!)
Resource Scaling: 10,000 Concurrent Connections
| Metric | Synchronous (1:1 OS Threads) |
Asynchronous (async / .await Tasks) |
|---|---|---|
| Memory per task | 2 MiB virtual stack (plus kernel thread structs) |
Exact size of live variables across .await (64–512 bytes!) |
| Memory for 10,000 idle connections | ~20 GiB virtual stack space |
~2–5 MiB total heap memory |
| Task switch cost | Kernel context switch (~1–2 µs + TLB/cache pressure) |
User-space function call Future::poll (~5–20 ns) |
| Task creation cost | OS syscall (clone / pthread_create, ~10–30 µs) |
Struct construction (0 syscalls, nanoseconds) |
| Cancellation | Cannot safely kill an arbitrary OS thread mid-flight | Dropping a Future cancels it cleanly at its next .await point |
Running a Zero-Dependency Synchronous vs. Asynchronous Comparison
Even using only the Rust standard library (std::future::Future, std::task), we can see how multiple async state machines interleave progress on a single thread without spawning extra OS threads:
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};
struct YieldOnce {
yielded: bool,
}
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: std::pin::Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
if self.yielded {
Poll::Ready(())
} else {
self.yielded = true;
Poll::Pending
}
}
}
async fn step_task(name: &str) -> &str {
println!("[{name}] step 1 (before await)");
YieldOnce { yielded: false }.await;
println!("[{name}] step 2 (after await)");
name
}
fn main() {
let mut task_a = pin!(step_task("Task A"));
let mut task_b = pin!(step_task("Task B"));
let mut cx = Context::from_waker(Waker::noop());
println!("--- Poll Round 1 (Both start and yield at .await) ---");
let _ = task_a.as_mut().poll(&mut cx);
let _ = task_b.as_mut().poll(&mut cx);
println!("--- Poll Round 2 (Both resume after .await and finish) ---");
let res_a = task_a.as_mut().poll(&mut cx);
let res_b = task_b.as_mut().poll(&mut cx);
println!("results: {:?}, {:?}", res_a, res_b);
}
--- Poll Round 1 (Both start and yield at .await) ---
[Task A] step 1 (before await)
[Task B] step 1 (before await)
--- Poll Round 2 (Both resume after .await and finish) ---
[Task A] step 2 (after await)
[Task B] step 2 (after await)
results: Ready("Task A"), Ready("Task B")