bigywater.tech
Rust · 98

async/await 状态机

.await 背后到底发生了什么?编译器把 async fn 变成一个枚举状态机,每个 await 点是一个状态,跨 await 的局部变量存在 Future 里。这篇用 size_of_val 直接量出状态机的大小,再验证它如何保存状态、为什么不能递归、为什么需要 Pin。

2 对 1002 字节
同一个数组放进内层块与留在函数作用域时,Future 的大小
E0733
async fn 直接递归的错误码
4 次
三个 await 的 async fn 从开始到 Ready 所需的 poll 次数

Future 的大小

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    async fn nothing()     的 Future 大小:     1 字节
    buf 在 await 前用完(仍在作用域内):       1002 字节
    buf 放进内层 { } 块,await 前作用域结束:      2 字节
    buf[1000] 跨 await 存活:                  1002 字节
    a[1000] + b[2000] 跨 await 存活:          3002 字节
    两个 await,只有一个 1000 字节数组跨第二个 await: 1002 字节
    YieldNow 自己:                               1 字节

Future 与 Poll 见Future 与 Poll。

状态保存与恢复

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    第 1 次 poll:
      [状态0] 开始
    第 2 次 poll:
      [await 之后] i = 1 sum = 1
    第 3 次 poll:
      [await 之后] i = 2 sum = 3
    第 4 次 poll:
      [await 之后] i = 3 sum = 6
    Ready(6),共 poll 4 次

完成后再 poll

实测 · 本机执行输出
  编译:成功
  运行(退出码 101):
    第一次 poll: Ready(1)
    第二次 poll(已完成):
    
    thread 'main' (7203531) panicked at <实验目录>/src/u98c.rs:7:21:
    `async fn` resumed after completion
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

递归

实测 · 本机执行输出
  源码:
      1 | async fn fib(n: u32) -> u32 { if n < 2 { n } else { fib(n - 1).await + fib(n - 2).await } }
      2 | fn main() { let _ = fib(5); }
  编译:失败
    error[E0733]: recursion in an async fn requires boxing
     --> <实验目录>/src/u98d.rs:1:1
      |
    1 | async fn fib(n: u32) -> u32 { if n < 2 { n } else { fib(n - 1).await + fib(n - 2).await } }
      | ^^^^^^^^^^^^^^^^^^^^^^^^^^^                         ---------------- recursive call here
      |
      = note: a recursive `async fn` call must introduce indirection such as `Box::pin` to avoid an infinitely sized future
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0733`.
  编译:成功
  运行(退出码 0):
    Ready(55)

自引用与 Pin

实测 · 本机执行输出
  源码:
      1 | #![allow(unused)]
      2 | use std::{future::Future, pin::Pin, task::{Context, Poll, Waker}, mem::size_of_val};
      3 | struct YieldNow(bool);
      4 | impl Future for YieldNow { type Output = (); fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { if self.0 { Poll::Ready(()) } else { self.0 = true; cx.waker().wake_by_ref(); Poll::Pending } } }
      5 | fn yield_now() -> YieldNow { YieldNow(false) }
      6 | 
      7 | fn is_unpin<T: Unpin>(_: &T) {}
      8 | async fn borrows() { let local = [1u8, 2, 3]; let r = &local; yield_now().await; println!("{}", r[0]); }
      9 | fn main() { let f = borrows(); is_unpin(&f); is_unpin(&yield_now()); }
  编译:失败
    error[E0277]: `{async fn body of borrows()}` cannot be unpinned
     --> <实验目录>/src/u98f.rs:9:41
      |
    8 | async fn borrows() { let local = [1u8, 2, 3]; let r = &local; yield_now().await; println!("{}", r[0]); }
      |                   - within this `impl Future<Output = ()>`
    9 | fn main() { let f = borrows(); is_unpin(&f); is_unpin(&yield_now()); }
      |                                -------- ^^ within `impl Future<Output = ()>`, the trait `Unpin` is not implemented for `{async fn body of borrows()}`
      |                                |
      |                                required by a bound introduced by this call
      |
      = note: consider using the `pin!` macro
              consider using `Box::pin` if you need to access the pinned value outside of the current scope
    note: required because it appears within the type `impl Future<Output = ()>`
     --> <实验目录>/src/u98f.rs:8:19
      |
    8 | async fn borrows() { let local = [1u8, 2, 3]; let r = &local; yield_now().await; println!("{}", r[0]); }
      |                   ^
    note: required by a bound in `is_unpin`
     --> <实验目录>/src/u98f.rs:7:16
      |
    7 | fn is_unpin<T: Unpin>(_: &T) {}
      |                ^^^^^ required by this bound in `is_unpin`
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0277`.

Pin 见Pin 与 Unpin。

分步演示

async/await 状态机
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。大小来自 std::mem::size_of_val
    • 没有验证:状态机的具体枚举布局(没有用 -Z dump-mir 看 coroutine)、更复杂的 select/join 的 Future 大小、-Zprint-type-sizes 的输出、编译器对内部变量布局的优化在其他版本是否一致
    京ICP备2026067502号-1京公网安备11010502063487号