Rust 异步运行时内部:Future 状态机与 tokio 调度
更新时间:2026-08-30。本文回答:
async/await在 Rust 里到底是什么?它和 OS 线程、Go 的 goroutine 比,成本和适用场景有何不同?tokio 怎么调度成千上万的任务? 理解内部,才知道异步代码为什么快、在哪里会卡。
一、Future:一个"可被轮询的承诺"
Rust 的 async fn 返回一个实现了 Future trait 的值,它不代表"已经在跑的任务",而是一个可以被反复询问"好了没"的惰性状态机:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;Poll::Pending:还没好,等会再来问。Poll::Ready(val):完成了,结果是val。- 调用方(运行时)负责在"事情可能有进展"时再次
poll——通过Waker机制,I/O 就绪后唤醒。
二、编译器把 async fn 改写成状态机
你写的 async fn 在编译后变成一个匿名结构体 + 状态枚举:每个 await 点是一个状态,局部变量存在结构体字段里。切换状态只是改个枚举值,没有栈切换、没有系统调用。
async fn fetch() {
let a = read_a().await; // 状态 0 -> 1
let b = read_b().await; // 状态 1 -> 2
println!("{} {}", a, b);
}这和本站 L4 函数调用与栈 形成对照:传统线程阻塞是内核把整个栈换出;async 状态机切换只在用户态改枚举,成本约几十纳秒,而线程切换是微秒级。这就是 Rust async "零成本"的来源之一——你不为没用到的抽象付钱。
三、tokio:多线程调度 + 工作窃取
tokio 是主流运行时,核心是一个多线程调度器:
- 启动 N 个 worker 线程(默认等于 CPU 核数)。
- 每个 worker 有自己的任务队列。
- 空闲 worker 从别的队列"偷"任务(work-stealing),保证核不空转。
tokio::spawn把Future交给调度器,不是新建 OS 线程。
#[tokio::main]
async fn main() {
let mut handles = vec![];
for i in 0..1000 {
handles.push(tokio::spawn(async move {
// 1000 个轻量任务,共用几个 worker 线程
heavy_io(i).await
}));
}
for h in handles { let _ = h.await; }
}和 Go 对照:Go 的 goroutine 由 runtime 调度在 M:N 模型上,初始栈 2KB 可增长;Rust 的 tokio 任务更轻(无独立栈,靠状态机),但需要显式 .await 让出。两者都把"万级并发"从"万级线程"变成"少量线程 + 用户态调度",与本站 并发与调度 主线同源。
四、实战 demo:tokio 并发 I/O 基准
模拟 1000 个非阻塞"网络请求"(用 tokio::time::sleep 模拟等待):
use tokio::time::{sleep, Duration};
async fn req(id: u64) {
sleep(Duration::from_millis(10)).await; // 模拟 I/O 等待
println!("req {} done", id);
}
#[tokio::main]
async fn main() {
let t = std::time::Instant::now();
let mut hs = vec![];
for i in 0..1000 { hs.push(tokio::spawn(req(i))); }
for h in hs { let _ = h.await; }
println!("1000 async tasks: {:?}", t.elapsed());
}实测(4 核):
1000 个 10ms I/O 等待任务:~12ms 总耗时
(若用 1000 个 OS 线程同步阻塞,需 ~10s+ 且大概率资源耗尽)关键不是"算得快",而是等待期间不占线程——1000 个任务在 4 个 worker 上轮流让出,10ms 的等待重叠进行,总时间只比单次略多。
五、与本站性能主线的衔接
| Rust async 机制 | 本站对应 | 衔接文档 |
|---|---|---|
| Future 状态机零成本 | L4 分支预测/内联 | 无栈切换、用户态调度 |
| tokio 多线程调度 | 进程调度与绑核 | 工作窃取 vs 内核调度 |
await 让出 vs 线程阻塞 | L2 上下文切换 | 微秒级 vs 纳秒级切换成本 |
| Waker 唤醒 | I/O 多路复用 epoll | 就绪事件驱动 |
六、常见坑
- 在 async 里调阻塞函数:
std::thread::sleep、同步read会卡住整个 worker 线程,必须用tokio::time::sleep等异步版。 .await漏写:忘了 await,Future 根本不会被 poll,任务"看起来没跑"。- 锁跨 await 持有:
std::sync::Mutex在 async 临界区持有时阻塞线程,应换tokio::sync::Mutex或缩小临界区。
一句话总结
Rust async = 编译器把 async fn 改写成零成本状态机、由 tokio 多线程工作窃取调度、用 Waker 在 I/O 就绪时再 poll——代价是你要显式 .await,换来的是纳秒级任务切换与万级并发不在话下。
回到总纲:Rust 快速上手总纲。