你的 Rust async 代码是个“骗子”:我把它的状态机底裤给扒了!

你有没有过这样的经历:你写了一个 async 函数,满心欢喜地调用了它,然后……程序“啪”一下就结束了,啥也没发生。

你盯着屏幕,陷入沉思:我的代码呢?我的 println! 呢?难道我刚才运行了个寂寞?

别慌,你不是一个人。欢迎来到 Rust async 的世界,这里的第一条规则就是:你眼睛看到的,不一定是真的。

async/await 是 Rust 并发编程的王牌,它能让你用看似同步的代码,写出性能炸裂的非阻塞程序,轻松应对成千上万的网络连接。但在这优雅的语法糖背后,藏着一个精妙绝伦、却也让无数新手迷惑的底层机制。

今天,我就带你潜入 Rust 的“引擎室”,把 Future.poll() 和状态机这几个核心部件给你安排得明明白白。

第一幕:天大的误会 —— async fn 根本不会立即执行

让我们从这个最基础的“骗局”开始。你写了这么一个函数:

async fn say_hello() {
    println!("Hello, from the future!");
}

然后你调用它:

fn main() {
    say_hello();
    // 程序结束,什么都不会打印
}

为什么?因为调用一个 async 函数,并不会执行它里面的代码。它只会返回一个东西,叫做 Future

什么是 Future

你可以把它想象成一张“未来才会兑现的承诺券”。

这张券本身什么都不是,你拿着它,它不会自动变成奖品。say_hello() 函数返回的就是这么一张承诺券,上面写着:“我承诺,未来某个时候会打印一行字”。

你的 main 函数拿到了这张券,然后就把它扔了,程序自然就结束了。

记住:async 函数返回的是一个“计划”,而不是一个“结果”。

第二幕:引擎启动 —— Executor.await 的登场

那么,怎么才能让这张“承诺券”兑现呢?

你需要一个“执行器”(Executor),比如大名鼎鼎的 tokioasync-std

你可以把执行器想象成一个精力无限的厨房总管

而你的 Future(承诺券),就是一张张“菜谱”。

当你把菜谱交给总管时,他才开始真正地干活。在代码里,这个“交由总管处理”的动作,通常是 #[tokio::main] 这个宏帮你做的,或者手动调用 block_on

#[tokio::main]
async fn main() {
    say_hello().await; // 这次,它会打印了!
}

注意那个神奇的 .await 关键字。它才是引爆这一切的“点火器”。

.await 到底做了什么?它对厨房总管(执行器)说:

“嘿,总管!我现在要开始做 say_hello 这道菜了。如果这道菜需要花时间(比如要等烤箱预热),你不用傻等,先去做别的菜。等我这儿好了,我再叫你。”

这就是 async/await 的核心:非阻塞.await 就是那个允许任务“暂停”和“让出控制权”的神奇节点。

第三幕:深入底层 —— Future 的真面目是“状态机”

现在,让我们揭开 Future 的最终面纱。Rust 编译器看到你的 async fn,会偷偷地把它变成一个结构体,并为它实现 Future Trait。

这个结构体,就是一个微型的“状态机”。

我们来看一个稍微复杂点的例子:

async fn cook_meal() {
    println!("开始洗菜...");
    let ingredients = chop_vegetables().await; // 第一个暂停点
    println!("菜切好了,开始炒菜...");
    let dish = fry_in_pan(ingredients).await; // 第二个暂停点
    println!("上菜!");
}

编译器会把这个函数转换成一个类似下面这样的状态机:

  • 状态0:初始状态
  • 状态1:正在切菜 (等待 chop_vegetables 完成)
  • 状态2:正在炒菜 (等待 fry_in_pan 完成)
  • 状态3:完成

厨房总管(执行器)会反复地过来问这个状态机一个问题,这个问题就是 .poll() 方法:“兄弟,你搞定了没?”

.poll() 方法会返回两种可能的结果:

  1. Poll::Ready(结果)搞定了! 这是我的结果,你可以进行下一步了。
  2. Poll::Pending还没好! 我正在等某个东西(比如网络数据、定时器)。你先去忙别的吧,等我好了,我会通过一个叫 Waker 的东西通知你再来问我。

整个流程就像这样:

  1. 总管调用 cook_mealpoll
  2. 代码执行 println!("开始洗菜...")
  3. 遇到 chop_vegetables().await,总管开始 poll 切菜任务。
  4. 切菜任务需要时间,返回 Pending。总管就把 cook_meal 任务放一边,去处理其他菜谱。
  5. 过了一会儿,“切菜机”(比如某个I/O设备)发来通知,通过 Waker 唤醒了 cook_meal 任务。
  6. 总管再次 poll 这个任务,它从上次暂停的地方继续,拿到了切好的菜。
  7. 遇到 fry_in_pan().await,流程再次重复。
  8. 直到最后,poll 返回 Ready,整个任务完成。

看到了吗?没有魔法。只有一台精密的、由 pollWaker 驱动的状态机。你的 async 代码,就在这台机器上,以一种高效、协作的方式,被一块块地执行。

总结:把大象装进冰箱

概念大白话比喻它的作用
async fn一张“菜谱”定义一个未来要执行的任务,但本身不执行。
Future菜谱的“承诺券”和“状态记录单”它是一个状态机,记录了任务进行到哪一步。
Executor (如 Tokio)精力无限的“厨房总管”管理和调度所有菜谱(Futures),让厨房不停运转。
.await“需要等待,但别干等”的指令任务的暂停点,把控制权交还给总管。
.poll()总管的口头禅:“搞定了没?”驱动状态机向前一步的方法。
Waker“烤箱好了”的“闹钟”当外部事件(如I/O)完成后,用来通知总管回来继续处理某个任务。

所以,下次当你的 async 代码表现得不符合直觉时,请记住,它不是在骗你。它只是在严格地、高效地遵循这套“厨房总管”的工作流程。而你,作为大厨,只需要写好你的菜谱,并在需要等待的地方优雅地加上一个 .await 就行了。


关注梦兽编程微信公众号,解锁更多黑科技