async / .await
async / .await
Level 9 — Rust Rust's zero-cost async abstraction:
async fnandasyncblocks produceFutures, while.awaitsuspends execution until the future resolves, driven by an executor like Tokio.
1. Prerequisites
async fn— Async functions.
2. Term Category
Rust Asynchronous Feature (cooperative task execution syntax): async and .await syntax construct zero-cost, cooperative asynchronous state machines (Futures) in Rust.
3. Explanation
(1) Design Motivation — "Why did we design this?"
Synchronous I/O operations (reading network sockets, waiting for database queries) block executing OS threads, requiring thousands of heavy OS threads for high-concurrency applications.
Rust's async/.await transforms asynchronous functions into lightweight cooperative state machines (Futures). When an async function encounters a .await yield point, it suspends execution and returns control to an asynchronous runtime executor (such as Tokio or async-executor) without blocking the underlying OS thread, enabling thousands of concurrent connections on a single OS thread.
(2) Reality Metaphor
A restaurant waiter serving 20 dining tables: after taking an order at Table 1 and handing it to the kitchen, the waiter does not stand idle in front of the kitchen waiting for food to cook; they immediately walk to Table 2 to take orders (.await yield). When food for Table 1 is ready, the kitchen notifies the waiter (waker notification), who resumes serving Table 1.
(3) Rust Code Examples
Basic Async Execution Pipeline
pub async fn compute_async_val(x: u32) -> u32 {
x * 2
}
pub async fn async_pipeline() -> u32 {
let val1 = compute_async_val(10).await;
let val2 = compute_async_val(20).await;
val1 + val2
}
fn block_on<F: std::future::Future>(mut fut: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
let mut pin_fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
match pin_fut.as_mut().poll(&mut cx) {
Poll::Ready(res) => res,
Poll::Pending => panic!("Future pending in sync runner"),
}
}
fn main() {
let result = block_on(async_pipeline());
assert_eq!(result, 60);
}
4. Common Mistakes & Pitfalls
Mistake 1: Invoking Synchronous Blocking I/O Inside async Tasks
The mistake: Invoking std::thread::sleep or std::fs::read inside async execution tasks.
Why it is wrong: Blocks the async runtime OS worker thread, preventing all other cooperative async tasks scheduled on that thread from making progress.
Incorrect:
async fn work() {
std::thread::sleep(std::time::Duration::from_secs(1)); // ❌ Blocks worker thread!
}
Fix:
async fn work() {
tokio::time::sleep(std::time::Duration::from_secs(1)).await; // Non-blocking yield!
}
Mistake 2: Calling an async fn Without Appending .await
The mistake: Invoking an async fn without appending .await to the call site.
Why it is wrong: In Rust, calling an async fn constructs a lazy Future state machine but does not execute it. The function body never runs unless polled or .awaited.
Incorrect:
fetch_data(); // ❌ Warning: unused Future! Code never executed!
Fix:
fetch_data().await; // Correct: awaits execution to completion!
Mistake 3: Holding Non-Send Mutex Guards Across .await Yield Points
The mistake: Holding a standard std::sync::MutexGuard across an .await yield point.
Why it is wrong: Causes compilation error Future is not Send because tasks moving between multi-threaded executor threads cannot hold non-Send guard references across suspension points.
Incorrect:
let guard = std_mutex.lock().unwrap();
fetch_remote_data().await; // ❌ Error: MutexGuard held across await!
Fix:
{
let guard = std_mutex.lock().unwrap();
// mutate data
} // Drop guard before await!
fetch_remote_data().await;
5. Practice Exercises
Exercise 1: Asynchronous HTTP Response Combinator Pipeline
Scenario: Build an asynchronous data processing pipeline combining results from multiple async tasks using .await.
Requirements:
- Define
async fn fetch_user_id(name: &str) -> u64. - Define
async fn fetch_user_balance(id: u64) -> u64. - Combine balance results in
async fn get_total_balance. - Write unit tests.
Answer
Implementation
pub async fn fetch_user_id(name: &str) -> u64 {
if name == "Alice" { 101 } else { 102 }
}
pub async fn fetch_user_balance(id: u64) -> u64 {
if id == 101 { 500 } else { 250 }
}
pub async fn get_total_balance(user1: &str, user2: &str) -> u64 {
let id1 = fetch_user_id(user1).await;
let id2 = fetch_user_id(user2).await;
let b1 = fetch_user_balance(id1).await;
let b2 = fetch_user_balance(id2).await;
b1 + b2
}
#[cfg(test)]
mod tests {
use super::*;
fn block_on_test<F: std::future::Future>(mut fut: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
let mut pin_fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
match pin_fut.as_mut().poll(&mut cx) {
Poll::Ready(res) => res,
Poll::Pending => panic!("Pending in sync test runner"),
}
}
#[test]
fn test_async_pipeline() {
let total = block_on_test(get_total_balance("Alice", "Bob"));
assert_eq!(total, 750);
}
}
Technical Explanation
- Async functions return lazy futures evaluated sequentially via
.await. .awaityields control back to the executor without blocking OS threads.- Pipelines combine multiple async data fetches cleanly.
Exercise 2: Async Retry Wrapper Mechanism
Scenario: Build an async retry helper function async_retry re-evaluating an async task up to N times on error.
Requirements:
- Implement
async fn async_retry<F, Fut, T, E>(mut f: F, max_retries: usize). - Write unit tests.
Answer
Implementation
pub async fn async_retry<F, Fut, T, E>(f: F, max_retries: usize) -> Result<T, E>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
{
let mut attempts = 0;
loop {
match f().await {
Ok(val) => return Ok(val),
Err(err) => {
attempts += 1;
if attempts >= max_retries {
return Err(err);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn block_on_test<F: std::future::Future>(mut fut: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
let mut pin_fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
match pin_fut.as_mut().poll(&mut cx) {
Poll::Ready(res) => res,
Poll::Pending => panic!("Pending"),
}
}
#[test]
fn test_async_retry_success() {
let res = block_on_test(async_retry(|| async { Ok::<i32, ()>(42) }, 3));
assert_eq!(res, Ok(42));
}
}
Technical Explanation
- Higher-order async functions accept closure factories returning
Futureinstances. .awaitevaluates each attempt asynchronously inside a retry loop.- Errors are returned after exhausting
max_retries.
Exercise 3: Async Task Identity and Polling Verification
Scenario: Validate cooperative yield state machine execution.
Requirements:
- Implement
async fn async_identity. - Write unit tests.
Answer
Implementation
pub async fn async_identity(val: i32) -> i32 {
val
}
#[cfg(test)]
mod tests {
use super::*;
fn block_on_test<F: std::future::Future>(mut fut: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
let mut pin_fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
match pin_fut.as_mut().poll(&mut cx) {
Poll::Ready(res) => res,
Poll::Pending => panic!("Pending"),
}
}
#[test]
fn test_async_identity() {
assert_eq!(block_on_test(async_identity(100)), 100);
}
}
Technical Explanation
- Tests fundamental
Futurepolling mechanics using standardWaker::noop. - Demonstrates zero-cost
Futurestate machine generation.
6. Related Terms
7. Key Takeaways
asyncfunctions transform code into state machines returningFuture..awaityields execution back to the runtime executor until the future is ready.- Do not invoke blocking synchronous functions (
std::thread::sleep) inside async tasks. - Async futures are lazy and perform zero work until polled or
.awaited.