异步编程的底层机制
原理
Rust 异步运行时基于协作式调度(cooperative scheduling)。async fn 在编译时被转换为实现了 Future trait 的状态机——每个 .await 点对应状态机的一个状态。
Future trait 的 poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> 方法被 executor 调用。Future 返回 Poll::Pending 表示未就绪,并通过 cx.waker().wake() 注册回调;返回 Poll::Ready(v) 表示完成。
Pin 保证 Future 在内存中不移动(自引用结构体的安全要求)。编译器生成的匿名类型包含所有跨 .await 的局部变量。
Executor(如 tokio)内部维护就绪队列和 IO 事件循环(epoll/io_uring)。tokio::spawn 将 Future 提交到线程池的任务队列中。
与 OS 线程对比:异步 Task 切换无内核参与(用户态切换),单个线程可管理数万个并发连接(C10K/C100K 问题解决)。
语法
// async fn — 编译器自动生成 Future 状态机
async fn fetch_url(url: &str) -> Result<String, Error> {
let resp = reqwest::get(url).await?;
resp.text().await
}
// tokio 运行时
#[tokio::main]
async fn main() {
let result = fetch_url("https://example.com").await.unwrap();
println!("{}", result);
}
// 并发等待
let (a, b) = tokio::join!(task1(), task2());
// select — 竞态
tokio::select! {
result = task_a() => { println!("a: {:?}", result); }
result = task_b() => { println!("b: {:?}", result); }
_ = tokio::signal::ctrl_c() => { println!("interrupted"); }
}Future 状态机展开(概念)
// async fn example(x: i32) -> i32 {
// let a = do_a(x).await;
// let b = do_b(a).await;
// b + 1
// }
// 展开为枚举:
// enum ExampleFuture { State0 { x: i32 }, State1 { a_future: ... }, ... }
// impl Future for ExampleFuture {
// fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<i32> { ... }
// }| 异步模型 | 线程 | Future |
|---|---|---|
| 调度 | OS 抢占 | 用户态协作 |
| 上下文切换 | ~1μs (内核) | ~20ns (函数调用) |
| 栈空间 | 2MB+ | 编译期确定(小) |
| 并发数 | 数千 | 百万级 |
| 开销 | 高 | 极低 |
实践
AI 自检
Pin<&mut Self>为何是 Future trait 的必要约束?自引用问题如何产生?- epoll 和 io_uring 的区别?io_uring 如何进一步减少系统调用开销?