15. Async/Await Essentials 🔴
15. Async/Await 核心要点 🔴
What you’ll learn:
本章将学到什么:
- How Rust’s
Futuretrait differs from Go’s goroutines and Python’s asyncio
Rust 的Futuretrait 和 Go goroutine、Python asyncio 到底差在哪- Tokio quick-start: spawning tasks,
join!, and runtime configuration
Tokio 快速上手:启动任务、使用join!、配置运行时- Common async pitfalls and how to fix them
常见 async 陷阱以及修法- When to offload blocking work with
spawn_blocking
什么时候该用spawn_blocking把阻塞工作甩出去
Futures, Runtimes, and async fn
Future、运行时与 async fn
Rust’s async model is fundamentally different from Go’s goroutines or Python’s asyncio. Understanding three concepts is enough to get started:
Rust 的 async 模型和 Go 的 goroutine、Python 的 asyncio 有 根本差异。真正入门只要先吃透三件事:
- A
Futureis a lazy state machine — callingasync fndoesn’t execute anything; it returns aFuturethat must be polled.
1.Future是惰性的状态机:调用async fn时什么都不会真正执行,它只会返回一个等待被 poll 的Future。 - You need a runtime to poll futures —
tokio,async-std, orsmol. The standard library definesFuturebut provides no runtime.
2. 必须有运行时 才能 poll future,比如tokio、async-std或smol。标准库只定义了Future,但压根没带运行时。 async fnis sugar — the compiler transforms it into a state machine that implementsFuture.
3.async fn只是语法糖:编译器会把它展开成一个实现了Future的状态机。
#![allow(unused)]
fn main() {
// A Future is just a trait:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
// async fn desugars to:
// fn fetch_data(url: &str) -> impl Future<Output = Result<Vec<u8>, Error>>
async fn fetch_data(url: &str) -> Result<Vec<u8>, reqwest::Error> {
let response = reqwest::get(url).await?; // .await yields until ready
let bytes = response.bytes().await?;
Ok(bytes.to_vec())
}
}
Tokio Quick Start
Tokio 快速上手
# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};
use tokio::task;
#[tokio::main]
async fn main() {
// Spawn concurrent tasks (like lightweight threads):
let handle_a = task::spawn(async {
sleep(Duration::from_millis(100)).await;
"task A done"
});
let handle_b = task::spawn(async {
sleep(Duration::from_millis(50)).await;
"task B done"
});
// .await both — they run concurrently, not sequentially:
let (a, b) = tokio::join!(handle_a, handle_b);
println!("{}, {}", a.unwrap(), b.unwrap());
}
Async Common Pitfalls
Async 常见陷阱
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Blocking in async 在 async 里做阻塞操作 | std::thread::sleep or CPU work blocks the executorstd::thread::sleep 或重 CPU 工作会把执行器线程直接卡死 | Use tokio::task::spawn_blocking or rayon用 tokio::task::spawn_blocking 或 rayon |
Send bound errorsSend 约束报错 | Future held across .await contains !Send type (e.g., Rc, MutexGuard)跨 .await 保存了 !Send 类型,例如 Rc、MutexGuard | Restructure to drop non-Send values before .await重构代码,让这些非 Send 值在 .await 之前就被释放 |
| Future not polled Future 根本没被 poll | Calling async fn without .await or spawning — nothing happens只调用 async fn 却没 .await,也没 spawn,结果就是什么都不会发生 | Always .await or tokio::spawn the returned future要么 .await,要么 tokio::spawn |
Holding MutexGuard across .await把 MutexGuard 跨 .await 持有 | std::sync::MutexGuard is !Send; async tasks may resume on different threadstd::sync::MutexGuard 是 !Send,而 async 任务恢复时可能换线程 | Use tokio::sync::Mutex or drop the guard before .await改用 tokio::sync::Mutex,或者在 .await 前先释放 guard |
| Accidental sequential execution 不小心写成串行执行 | let a = foo().await; let b = bar().await; runs sequentiallylet a = foo().await; let b = bar().await; 天然就是顺序执行 | Use tokio::join! or tokio::spawn for concurrency想并发就用 tokio::join! 或 tokio::spawn |
#![allow(unused)]
fn main() {
// ❌ Blocking the async executor:
async fn bad() {
std::thread::sleep(std::time::Duration::from_secs(5)); // Blocks entire thread!
}
// ✅ Offload blocking work:
async fn good() {
tokio::task::spawn_blocking(|| {
std::thread::sleep(std::time::Duration::from_secs(5)); // Runs on blocking pool
}).await.unwrap();
}
}
Comprehensive async coverage: For
Stream,select!, cancellation safety, structured concurrency, andtowermiddleware, see our dedicated Async Rust Training guide. This section covers just enough to read and write basic async code.
更完整的 async 内容:如果需要继续看Stream、select!、取消安全、结构化并发和tower中间件,请直接去看单独的 Async Rust Training。这一节的目标只是让人能读懂并写出基础 async 代码。
Spawning and Structured Concurrency
任务生成与结构化并发
Tokio’s spawn creates a new asynchronous task — similar to thread::spawn but much lighter:
Tokio 的 spawn 会创建一个新的异步任务,概念上类似 thread::spawn,但成本轻得多:
use tokio::task;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// Spawn three concurrent tasks
let h1 = task::spawn(async {
sleep(Duration::from_millis(200)).await;
"fetched user profile"
});
let h2 = task::spawn(async {
sleep(Duration::from_millis(100)).await;
"fetched order history"
});
let h3 = task::spawn(async {
sleep(Duration::from_millis(150)).await;
"fetched recommendations"
});
// Wait for all three concurrently (not sequentially!)
let (r1, r2, r3) = tokio::join!(h1, h2, h3);
println!("{}", r1.unwrap());
println!("{}", r2.unwrap());
println!("{}", r3.unwrap());
}
join! vs try_join! vs select!:join!、try_join! 和 select! 的区别:
| Macro | Behavior | Use when |
|---|---|---|
join!join! | Waits for ALL futures 等待所有 future 完成 | All tasks must complete 所有任务都必须完成时 |
try_join!try_join! | Waits for all, short-circuits on first Err等待全部,但一遇到 Err 就提前返回 | Tasks return Result任务返回值是 Result 时 |
select!select! | Returns when FIRST future completes 哪个 future 先完成就先返回 | Timeouts, cancellation 超时、取消等场景 |
use tokio::time::{timeout, Duration};
async fn fetch_with_timeout() -> Result<String, Box<dyn std::error::Error>> {
let result = timeout(Duration::from_secs(5), async {
// Simulate slow network call
tokio::time::sleep(Duration::from_millis(100)).await;
Ok::<_, Box<dyn std::error::Error>>("data".to_string())
}).await??; // First ? unwraps Elapsed, second ? unwraps inner Result
Ok(result)
}
Send Bounds and Why Futures Must Be Send
Send 约束,以及为什么 future 往往必须是 Send
When you tokio::spawn a future, it may resume on a different OS thread. This means the future must be Send. Common pitfalls:
当用 tokio::spawn 启动一个 future 时,它后续恢复执行的位置可能已经换成另一个操作系统线程了。所以这个 future 通常必须实现 Send。最常见的坑就在这里:
use std::rc::Rc;
async fn not_send() {
let rc = Rc::new(42); // Rc is !Send
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!("{}", rc); // rc is held across .await — future is !Send
}
// Fix 1: Drop before .await
async fn fixed_drop() {
let data = {
let rc = Rc::new(42);
*rc // Copy the value out
}; // rc dropped here
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!("{}", data); // Just an i32, which is Send
}
// Fix 2: Use Arc instead of Rc
async fn fixed_arc() {
let arc = std::sync::Arc::new(42); // Arc is Send
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!("{}", arc); // ✅ Future is Send
}
Comprehensive async coverage: For
Stream,select!, cancellation safety, structured concurrency, andtowermiddleware, see our dedicated Async Rust Training guide. This section covers just enough to read and write basic async code.
更完整的 async 内容:Stream、select!、取消安全、结构化并发和tower中间件这些主题,还是继续看专门的 Async Rust Training 更合适。本节只负责把基础 async 写法讲明白。
See also: Ch 5 — Channels for synchronous channels. Ch 6 — Concurrency for OS threads vs async tasks.
继续阅读: 第 5 章:Channel 讲同步 channel,第 6 章:并发 会对比操作系统线程和 async 任务。
Key Takeaways — Async
本章要点:Async
async fnreturns a lazyFuture— nothing runs until you.awaitor spawn itasync fn返回的是惰性Future,只有.await或 spawn 之后它才会真正运行。- Use
tokio::task::spawn_blockingfor CPU-heavy or blocking work inside async contexts
在 async 上下文里遇到重 CPU 或阻塞工作时,用tokio::task::spawn_blocking把它甩出去。- Don’t hold
std::sync::MutexGuardacross.await— usetokio::sync::Mutexinstead
不要把std::sync::MutexGuard跨.await持有,异步场景里改用tokio::sync::Mutex。- Futures must be
Sendwhen spawned — drop!Sendtypes before.awaitpoints
被 spawn 的 future 往往必须是Send,因此在.await之前就要把!Send的值释放掉。
Exercise: Concurrent Fetcher with Timeout ★★ (~25 min)
练习:带超时的并发抓取器 ★★(约 25 分钟)
Write an async function fetch_all that spawns three tokio::spawn tasks, each simulating a network call with tokio::time::sleep. Join all three with tokio::try_join! wrapped in tokio::time::timeout(Duration::from_secs(5), ...). Return Result<Vec<String>, ...> or an error if any task fails or the deadline expires.
写一个异步函数 fetch_all,内部启动三个 tokio::spawn 任务,每个任务都用 tokio::time::sleep 模拟一次网络调用。然后用 tokio::try_join! 把它们合并,并且整个过程外面套上一层 tokio::time::timeout(Duration::from_secs(5), ...)。如果任一任务失败,或者总超时到了,就返回错误;否则返回 Result<Vec<String>, ...>。
🔑 Solution 🔑 参考答案
use tokio::time::{sleep, timeout, Duration};
async fn fake_fetch(name: &'static str, delay_ms: u64) -> Result<String, String> {
sleep(Duration::from_millis(delay_ms)).await;
Ok(format!("{name}: OK"))
}
async fn fetch_all() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let deadline = Duration::from_secs(5);
let (a, b, c) = timeout(deadline, async {
let h1 = tokio::spawn(fake_fetch("svc-a", 100));
let h2 = tokio::spawn(fake_fetch("svc-b", 200));
let h3 = tokio::spawn(fake_fetch("svc-c", 150));
tokio::try_join!(h1, h2, h3)
})
.await??;
Ok(vec![a?, b?, c?])
}
#[tokio::main]
async fn main() {
let results = fetch_all().await.unwrap();
for r in &results {
println!("{r}");
}
}