【Rust】16-async/await、Future 与执行器模型
async/await、Future 与执行器模型
研究目标
- 理解 Rust async 不是创建线程,而是生成状态机。
- 掌握
Future、Waker、executor 的基本关系。 - 知道 async 代码中所有权、生命周期和 Send 约束为何常见。
async 的核心模型
Rust 的async fn会返回一个实现了Future的值。调用 async 函数本身并不会立即执行完整逻辑,它只是构造一个 future。
asyncfnfetch()->String{String::from("data")}fnmain(){letfuture=fetch();// future 还没有被执行到完成}future 必须被执行器轮询,才会向前推进。常见执行器包括 Tokio、async-std、smol,也可以在测试或嵌入式环境中使用专门执行器。
Future Trait
简化后的Futuretrait 可以理解为:
usestd::pin::Pin;usestd::task::{Context,Poll};traitFuture{typeOutput;fnpoll(self:Pin<&mutSelf>,cx:&mutContext<'_>)->Poll<Self::Output>;}poll返回两种状态:
Poll::Ready(value):计算完成。Poll::Pending:暂时不能完成,稍后再来轮询。
执行器反复轮询 future。当 future 因等待 IO、定时器或其他事件无法继续时,返回Pending,并通过Waker告诉执行器将来什么时候再唤醒它。
await 做了什么
.await会在当前 future 内等待另一个 future 完成:
asyncfnhandle()->usize{lettext=read_text().await;text.len()}asyncfnread_text()->String{String::from("hello")}编译器会把handle转换成状态机。read_text().await是一个可能暂停的位置。暂停时,当前函数的局部变量需要被保存到 future 对象内部,等唤醒后继续执行。
async 状态机
可以把 async 函数想象成枚举状态机:
enumHandleFuture{Start,WaitingReadText,Done,}真实生成代码更复杂,但关键直觉是:跨越.await的局部变量会成为 future 状态的一部分。这也是为什么 async 代码经常遇到所有权和生命周期问题。
Pin 的作用
某些 future 内部可能自引用:状态机里一个字段引用另一个字段。这样的对象一旦被移动,内部引用就可能失效。Pin用于表达“这个值不能再被随意移动”的约束。
普通用户很少需要手写Pin,但理解它有助于解释为什么Future::poll的接收者是Pin<&mut Self>。async/await 让这些复杂性大多被编译器和运行时封装起来。
Waker 与唤醒
当 future 返回Pending时,它必须确保在将来可以继续时调用 waker:
executor poll future future waits for IO future stores waker future returns Pending IO ready waker wakes task executor polls future again如果 future 返回Pending但没有正确安排唤醒,任务可能永远卡住。执行器和 IO reactor 的配合负责处理这些细节。
执行器模型
执行器负责调度任务。一个常见模型是:
- 任务队列保存可运行 future。
- 执行器 poll 某个任务。
- 如果 Ready,任务完成。
- 如果 Pending,任务让出执行权。
- 外部事件通过 waker 把任务放回队列。
这和操作系统线程不同。async 任务通常是协作式调度:只有在.await等挂起点才会让出执行权。一个没有 await 的长 CPU 循环会阻塞同一执行器线程上的其他任务。
Tokio 示例
#[tokio::main]asyncfnmain(){lettask=tokio::spawn(async{"hello"});letresult=task.await.unwrap();println!("{result}");}tokio::spawn通常要求 future 是Send + 'static,因为任务可能在线程池中被移动到其他线程执行,并且执行器不能依赖当前栈帧里的短生命周期引用。
Send 约束常见来源
下面的模式容易出问题:
usestd::rc::Rc;asyncfnwork(){letvalue=Rc::new(1);some_async().await;println!("{value}");}asyncfnsome_async(){}Rc<T>不是Send。如果value跨越.await存活,那么整个 future 可能不是Send。在多线程执行器中,这类 future 不能被spawn。
修复方式取决于需求:
- 使用
Arc<T>替代Rc<T>。 - 让非 Send 值不跨越
.await。 - 使用单线程执行器或
spawn_local。
async 与借用
跨 await 持有借用也需要谨慎:
asyncfnprint_later(text:&str){wait().await;println!("{text}");}asyncfnwait(){}这个 future 的生命周期依赖text。如果要把它放入要求'static的任务中,就不能借用当前栈上的字符串。常见做法是传入拥有所有权的String或Arc<str>。
阻塞操作
async 代码中不能随意执行阻塞操作:
std::thread::sleep(std::time::Duration::from_secs(1));这会阻塞执行器线程。应使用运行时提供的异步版本:
tokio::time::sleep(std::time::Duration::from_secs(1)).await;文件 IO、数据库驱动、HTTP 客户端也应选择 async 兼容版本,或者放到专门的 blocking 线程池。
常见误解
async fn调用后不会自动跑完,必须被 await 或 spawn。- async 不等于并行;它主要解决等待期间让出执行权。
.await是可能暂停点,跨越它的变量会影响 future 类型。Send + 'static错误通常来自任务调度模型,不是编译器无理限制。
继续研究
- Rust Async Book:Future、task wakeups、executor。
- Rust Reference:async functions、async blocks、await expressions。
- Tokio 文档:runtime、task、spawn、spawn_blocking。
- futures crate:FutureExt、Stream、select、join。
后记
2026年6月11日15点21分于上海。
