Reading — step 1 of 7
Learn
Async Rust lets you write code that LOOKS synchronous but yields control while waiting on I/O. Programming Rust dedicates a chapter to this; the Async Book is the canonical reference.
Note: This is conceptual — running async code requires a runtime like Tokio, which Judge0 doesn't include. We'll show the syntax.
async fn — futures
Marking a function async makes it return a Future:
async fn fetch_data() -> String {
// ... await some I/O ...
String::from("data")
}
// fetch_data() doesn't run anything — it returns a Future.
let fut = fetch_data(); // Future, not String
Futures are LAZY in Rust. Calling an async fn just creates a Future. Nothing runs until the future is polled by an executor.
.await — yielding for async results
Inside an async context, .await extracts the value from a Future, suspending until the value is ready:
async fn build_response() -> String {
let user = fetch_user().await; // wait for fetch_user's future
let settings = fetch_settings().await;
format!("{} - {}", user, settings)
}
.await CAN ONLY appear inside an async fn or async block. The compiler transforms the function into a state machine that yields at each .await point.
Running futures: the executor
A future does nothing until polled. You need a runtime:
// With tokio:
#[tokio::main]
async fn main() {
let s = fetch_data().await;
println!("{}", s);
}
// With async-std:
fn main() {
async_std::task::block_on(async {
let s = fetch_data().await;
println!("{}", s);
});
}
The runtime polls futures, schedules them, and parks them on I/O readiness via the OS (epoll/kqueue/iocp).
Concurrency primitives
use tokio::time::{sleep, Duration};
async fn parallel_work() {
// Run two futures concurrently — both make progress
let (a, b) = tokio::join!(
async { sleep(Duration::from_millis(100)).await; 1 },
async { sleep(Duration::from_millis(100)).await; 2 },
);
println!("got {} and {}", a, b);
}
tokio::spawn puts a future on the runtime; tokio::join! waits for multiple to complete; tokio::select! races them.
Async vs threads
- Threads — each task gets its own OS thread (cheap-ish, but context switches and memory cost)
- Async — many tasks multiplexed onto fewer threads (memory-cheap, suited for I/O-bound work)
For a server handling 10k concurrent connections, async is the answer. For CPU-bound parallelism, threads (often via rayon) are better.
Pin and self-referential structs
Futures can be self-referential — they hold pointers into their own state machine. This means they can't be moved after polling has begun. Pin is the type that prevents moving:
use std::pin::Pin;
use std::future::Future;
fn poll_once<F: Future>(fut: Pin<&mut F>) {
// ...
}
Most user code never touches Pin directly — just .await. It matters when implementing custom futures or working with tokio::pin!.
Send and Sync for futures
A future returned from an async fn is Send if all variables it holds across .await points are Send. Rc (not Send) inside an async fn breaks this:
async fn bad() {
let local = Rc::new(5);
other_async_fn().await;
println!("{}", local); // local held across await — future is !Send
}
If you need Rc, drop it before the .await; for thread-spanning, use Arc.
Common mistakes
- Creating a future and never awaiting it — futures do nothing on their own. The compiler usually warns.
- Blocking inside async — calling
std::thread::sleepor sync I/O blocks the entire executor thread. Use the async equivalents. - Holding non-Send types across .await — breaks tokio::spawn.
- Spawning hundreds of tasks for CPU work — async is for I/O. Use rayon or threads for CPU.
- Async trait methods — historically required boxing (
async-traitcrate orBox<dyn Future>). Native async fn in traits stabilized in 1.75 but with caveats.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…