在 Rust 异步接口的丛林中生存:从同步 I/O 到手写异步状态机

本文是对 Surviving Rust async interfaces 的整理与翻译

我曾经很害怕 async Rust。很容易掉坑里。

但随着社区的持续努力,async Rust 每周都在变得更容易使用。这篇文章从一个具体的任务出发------计算文件的 SHA3-256 哈希值------从同步实现开始,一步一步引入异步,踩遍所有该踩的坑,最终手工实现一个正确的异步状态机适配器。

涉及的核心问题包括:PinUnpin 到底在做什么、async-trait 的局限性、Send 约束的来源、spawn vs spawn_local 的区别,以及------最关键的------Future 被 drop 就等于被取消这一往往被忽视的语义。


一、项目初始化与工具依赖

先用 cargo new 创建项目,然后添加几个依赖:

bash 复制代码
$ cargo new surviving
$ cargo add argh sha3 color-eyre

color-eyre 提供了一个功能完善的 Error 类型,集成了带彩色高亮的 backtrace 和 spantrace,初始化只需一行:

rust 复制代码
use color_eyre::eyre;
use std::io;

fn main() -> Result<(), eyre::Error> {
    color_eyre::install().unwrap();

    let e = io::Error::new(
        io::ErrorKind::PermissionDenied,
        "you can't cut back on error handling! you will regret it",
    );
    Err(e.into())
}

设置 RUST_BACKTRACE=1 后,错误输出带有彩色的调用栈,非常方便调试。

argh 是一个声明式命令行参数解析库,基于 derive 宏,和 structopt 类似但体积更小。它支持 PathBuf 作为参数类型,因此不必自己处理字符串转路径的问题:

rust 复制代码
use argh::FromArgs;
use std::path::PathBuf;

/// Prints the SHA3-256 hash of a file.
#[derive(FromArgs)]
struct Args {
    /// the file whose contents to hash and print
    #[argh(positional)]
    file: PathBuf,
}

测试一下:

bash 复制代码
$ cargo run -q -- /etc/hosts
/etc/hosts is 184 bytes

二、同步版本:计算 SHA3-256

SHA-3 是 Keccak 海绵函数族的一个子集,2015 年由 Bertoni、Daemen、Peeters 和 Assche 发布。它有多种摘要长度,我们使用 256 位版本。

先用 OpenSSL 计算一个参考值:

bash 复制代码
$ openssl dgst -sha3-256 wine-5.0.2.tar.xz
SHA3-256(wine-5.0.2.tar.xz)= 2a48ac5363318b2b8dd222933002bac9fa0e1cc051605c17ebdae9f78ff03892

用 Rust 实现同步版本,核心是一个读-喂-哈希的循环:

rust 复制代码
use argh::FromArgs;
use color_eyre::eyre;
use sha3::Digest;
use std::{fs::File, io::Read, path::PathBuf};

/// Prints the SHA3-256 hash of a file.
#[derive(FromArgs)]
struct Args {
    #[argh(positional)]
    file: PathBuf,
}

fn main() -> Result<(), eyre::Error> {
    color_eyre::install().unwrap();
    let args: Args = argh::from_env();

    let mut file = File::open(&args.file)?;
    let mut hasher = sha3::Sha3_256::new();

    let mut buf = vec![0u8; 256 * 1024];
    loop {
        let n = file.read(&mut buf[..])?;
        match n {
            0 => break,
            n => hasher.update(&buf[..n]),
        }
    }

    let hash = hasher.finalize();
    print!("{} ", args.file.display());
    for x in hash {
        print!("{:02x}", x);
    }
    println!();

    Ok(())
}

运行:

bash 复制代码
$ cargo build --release -q && ./target/release/surviving wine-5.0.2.tar.xz
wine-5.0.2.tar.xz 2a48ac5363318b2b8dd222933002bac9fa0e1cc051605c17ebdae9f78ff03892

结果与 OpenSSL 完全一致。


三、引入 async:async-std#[async_std::main]

添加 async-std,并启用 attributes feature:

toml 复制代码
# in Cargo.toml
async-std = { version = "1.6.2", features = ["attributes"] }

main 函数改成 async:

rust 复制代码
#[async_std::main]
async fn main() -> Result<(), eyre::Error> {
   // 与之前相同
}

运行结果不变。但------此时代码里其实没有任何真正的异步操作,所有 I/O 都是阻塞的。

3.1 async-std 到底用了几个线程?

用 GDB 打断点来看一下:

bash 复制代码
$ gdb --args ./target/debug/surviving ./wine-5.0.2.tar.xz
(gdb) break pthread_create
(gdb) r
Breakpoint 1, 0x00007ffff7f6d700 in pthread_create@@GLIBC_2.2.5

调用栈清楚地显示:async_std::rt::RUNTIME 创建了线程。是的,async-std 用了线程池。但此时我们的代码是顺序执行的------打开文件、读取、喂给 hasher,没有并行。

接下来,让文件读取真正变成异步:

rust 复制代码
use async_std::{fs::File, io::ReadExt};

#[async_std::main]
async fn main() -> Result<(), eyre::Error> {
    color_eyre::install().unwrap();
    let args: Args = argh::from_env();

    let mut file = File::open(&args.file).await?;
    let mut hasher = sha3::Sha3_256::new();

    let mut buf = vec![0u8; 256 * 1024];
    loop {
        let n = file.read(&mut buf[..]).await?;  // 异步读取
        match n {
            0 => break,
            n => hasher.update(&buf[..n]),
        }
    }
    // ...
}

四、并行哈希多个文件:spawn 与线程调度

支持同时哈希多个文件,需要 spawn

rust 复制代码
/// Prints the SHA3-256 hash of some files
#[derive(FromArgs)]
struct Args {
    #[argh(positional)]
    files: Vec<PathBuf>,
}

#[async_std::main]
async fn main() -> Result<(), eyre::Error> {
    color_eyre::install().unwrap();
    let args: Args = argh::from_env();

    let mut handles = Vec::new();

    for file in &args.files {
        let file = file.clone();
        let handle = async_std::task::spawn(async move {
            let res = hash_file(&file).await;
            if let Err(e) = res {
                println!("While hashing {}: {}", file.display(), e);
            }
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.await;
    }

    Ok(())
}

async fn hash_file(path: &Path) -> Result<(), eyre::Error> {
    let mut file = File::open(path).await?;
    let mut hasher = sha3::Sha3_256::new();

    let mut buf = vec![0u8; 256 * 1024];
    loop {
        let n = file.read(&mut buf[..]).await?;
        match n {
            0 => break,
            n => hasher.update(&buf[..n]),
        }
    }

    let hash = hasher.finalize();
    print!("{} ", path.display());
    for x in hash {
        print!("{:02x}", x);
    }
    println!();

    Ok(())
}

运行正常。但任务有没有在多线程上跑?在循环里加一行打印:

rust 复制代码
println!("{} => {:?}", path.display(), std::thread::current().id());

输出:

ini 复制代码
wine-4.0.4.tar.xz => ThreadId(3)
wine-5.0.1.tar.xz => ThreadId(3)
wine-5.0.2.tar.xz => ThreadId(2)
wine-5.0.1.tar.xz => ThreadId(3)
wine-5.0.2.tar.xz => ThreadId(1)
wine-4.0.4.tar.xz => ThreadId(2)

任务不仅在多个线程上跑,同一个任务(比如 wine-4.0.4.tar.xz)在不同的 read 之间还会从 ThreadId(3) 跳到 ThreadId(2) 再到 ThreadId(1)。这证明了 async-std 是一个抢占式多线程工作窃取调度器------任务在 await 点可以被调度到任意线程继续执行。


五、实现自定义 AsyncReadPin 的本质

为了进一步理解调度行为,我们想实现一个"追踪读取器"------在每次 read 时打印当前线程 ID 和内存地址。这需要实现 AsyncRead trait。

先添加 futures crate:

bash 复制代码
$ cargo add futures

5.1 AsyncRead 的签名与 std::io::Read 的区别

rust 复制代码
use futures::io::AsyncRead;
use std::{
    io,
    pin::Pin,
    task::{Context, Poll},
};

struct TracingReader<R>
where
    R: AsyncRead,
{
    inner: R,
}

impl<R> AsyncRead for TracingReader<R>
where
    R: AsyncRead,
{
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut [u8],
    ) -> Poll<io::Result<usize>> {
        todo!()
    }
}

std::io::Read 相比,AsyncRead::poll_read 有三处不同:

  • self: Pin<&mut Self> 而非 &mut selfPin 保证 self 不会被移动
  • cx: &mut Context<'_>:执行器的唤醒上下文,用于注册"I/O 就绪时叫我"的回调
  • 返回 Poll<io::Result<usize>> 而非 io::Result<usize>Poll::Ready 表示立即完成,Poll::Pending 表示需要等待

5.2 Pin 是什么,为什么存在?

Pin 的核心语义:保证某个值的内存地址不会改变

为什么需要这个保证?因为异步函数生成的 Future 可能是自引用结构------Future 内部的某个字段持有指向自身另一个字段的指针。如果这个 Future 被 move 到了不同的内存地址,那些内部指针就会变成悬垂指针。Pin 就是用来防止这种 move 发生的。

TracingReader 加上 pin 之后,要获取 self.inner&mut R 或者 Pin<&mut R>,需要用特殊的方式。

5.3 不安全的方式:get_unchecked_mutmap_unchecked_mut

方式一:获取 &mut R(不推荐直接用于 poll_read)

rust 复制代码
let inner: &mut R = unsafe { &mut self.get_unchecked_mut().inner };

但是 &mut R 不能调用 poll_read,因为 poll_read 要求 Pin<&mut Self>

方式二:获取 Pin<&mut R>(结构性 pinning)

rust 复制代码
fn poll_read(
    self: Pin<&mut Self>,
    cx: &mut Context<'_>,
    buf: &mut [u8],
) -> Poll<io::Result<usize>> {
    // 打印追踪信息
    let address = &self as *const _;
    println!("{:?} => {:?}", address, std::thread::current().id());

    // 结构性 pinning:inner 被 pin 住了
    let inner: Pin<&mut R> = unsafe { self.map_unchecked_mut(|x| &mut x.inner) };
    inner.poll_read(cx, buf)
}

map_unchecked_mut 的语义是:我保证字段 innerself 被 pin 的期间也是 pin 住的------这叫做"结构性 pinning(structural pinning)"。

这段代码能工作,但使用了 unsafe。问题在于:类型系统无法阻止你对同一个字段同时调用 get_unchecked_mut(取得 &mut R,可以移动)和 map_unchecked_mut(取得 Pin<&mut R>,保证不移动),这两件事相互矛盾,如果同时做就会 UB。

5.4 更好的方式:pin-project

bash 复制代码
$ cargo add pin-project

pin-project 让你用属性声明哪些字段是结构性 pinning 的,哪些不是,然后自动生成类型安全的投影方法:

rust 复制代码
use pin_project::pin_project;

#[pin_project]
struct TracingReader<R>
where
    R: AsyncRead,
{
    #[pin]  // 声明 inner 是结构性 pinning
    inner: R,
}

impl<R> AsyncRead for TracingReader<R>
where
    R: AsyncRead,
{
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut [u8],
    ) -> Poll<io::Result<usize>> {
        let address = &self as *const _;
        println!("{:?} => {:?}", address, std::thread::current().id());

        // self.project() 生成投影,#[pin] 字段得到 Pin<&mut T>,其余字段得到 &mut T
        self.project().inner.poll_read(cx, buf)
    }
}

现在代码里没有任何 unsafe,并且类型系统保证了不会出现"又 pin 又 unpin"的矛盾。

5.5 观察线程调度

hash_file 里使用 TracingReader

rust 复制代码
async fn hash_file(path: &Path) -> Result<(), eyre::Error> {
    let file = File::open(path).await?;
    let mut file = TracingReader { inner: file };

    let mut buf = vec![0u8; 256 * 1024];
    loop {
        let n = file.read(&mut buf[..]).await?;
        match n {
            0 => break,
            n => hasher.update(&buf[..n]),
        }
    }
    // ...
}

输出:

ini 复制代码
0x7f54804343e0 => ThreadId(2)
0x7f54804343e0 => ThreadId(2)
0x7ffff9685970 => ThreadId(1)
0x7f547b3f83e0 => ThreadId(10)
0x7f54804343e0 => ThreadId(2)

每个地址代表一个 TracingReader 实例(也就是一个文件的哈希任务),可以看到它们确实在不同线程上交替推进。


六、Trait 中的 async fn:当前的限制与 async-trait

现在我们想定义一个更简洁的接口------一个带 async fn 的 trait。

6.1 Trait 中不能直接写 async fn(2020年时)

rust 复制代码
trait SimpleRead {
    async fn simple_read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}
rust 复制代码
error[E0706]: functions in traits cannot be declared `async`
  = note: `async` trait functions are not currently supported
  = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait

编译器直接在错误信息里推荐了 async-trait

注:这是 2020 年的情况。async fn in traits 已于 2023 年 12 月(Rust 1.75)正式稳定。

6.2 async-trait 的工作原理与 Send 约束

bash 复制代码
$ cargo add async-trait
rust 复制代码
use async_trait::async_trait;

#[async_trait]
trait SimpleRead {
    async fn simple_read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}

async-trait 的本质是一个过程宏,它把 async fn 改写成返回 Pin<Box<dyn Future + Send>> 的普通函数。默认情况下,它要求返回的 Future 是 Send 的------因为在多线程执行器上,Future 可能被调度到不同线程,它必须可以安全地跨线程发送。


七、Pin、Send、Unpin:三重关卡

7.1 第一关:Pin<&mut Self> 作为 receiver

为了在实现中能调用 inner.read(buf).awaitAsyncRead 系列方法需要 Pin<&mut R>),我们把方法 receiver 改成 Pin<&mut Self>

rust 复制代码
#[async_trait]
trait SimpleRead {
    async fn simple_read(self: Pin<&mut Self>, buf: &mut [u8]) -> io::Result<usize>;
}

#[async_trait]
impl<R> SimpleRead for TracingReader<R>
where
    R: AsyncRead,
{
    async fn simple_read(self: Pin<&mut Self>, buf: &mut [u8]) -> io::Result<usize> {
        self.project().inner.read(buf).await
    }
}
rust 复制代码
error: future cannot be sent between threads safely
  = help: the trait `std::marker::Send` is not implemented for `R`

7.2 第二关:R: Send

Future 不是 Send 的,因为 R 不是 Send。加上约束:

rust 复制代码
impl<R> SimpleRead for TracingReader<R>
where
    R: AsyncRead + Send,
rust 复制代码
error: future cannot be sent between threads safely
  - has type `*const std::pin::Pin<&mut TracingReader<R>>` which is not `Send`
  note: future is not `Send` as this value is used across an `await`

7.3 第三关:裸指针跨 await 的生命周期问题

追踪代码里的 let address = &self as *const _ 产生了一个裸指针。裸指针不是 Send。编译器认为 address 可能在 await 之后仍然被使用(因为 Drop 在作用域末尾才运行),所以拒绝了这个 Future 是 Send 的判断。

解决方案:用花括号把 address 的生命周期限制在 await 之前:

rust 复制代码
async fn simple_read(self: Pin<&mut Self>, buf: &mut [u8]) -> io::Result<usize> {
    {
        // 追踪代码在独立作用域内,await 之前就被 drop
        let address = &self as *const _;
        println!("{:?} => {:?}", address, std::thread::current().id());
    }
    self.project().inner.read(buf).await
}

编译通过。

7.4 Pin<&mut Self> 的调用方式

因为 simple_read 的 receiver 是 Pin<&mut Self>,调用方需要手动 pin:

rust 复制代码
let n = Pin::new(&mut file).simple_read(&mut buf[..]).await?;

但是这样又遇到了 async_std::task::spawn 要求 Future 是 Send 的问题,因为此时用的是 ?Send 版本的 trait。

7.5 使用 spawn_local(单线程执行器)

toml 复制代码
async-std = { version = "1.6.2", features = ["attributes", "unstable"] }

spawn 改成 spawn_local,任务就在单个线程上协作调度:

ini 复制代码
0x55a274428fe8 => ThreadId(1)
0x55a2744292c8 => ThreadId(1)
0x55a274429458 => ThreadId(1)
0x55a274428fe8 => ThreadId(1)

所有任务都在 ThreadId(1) 上,但三个文件的任务(地址不同)交替推进。单线程执行器依然能做到 I/O 并发------当一个任务等待 I/O 时,调度器切换到另一个任务。

7.6 最终选择:让 R: Unpin,恢复 &mut self

如果不想手动 pin,可以要求 R: Unpin,这样可以直接用 &mut self

rust 复制代码
#[async_trait]
impl<R> SimpleRead for TracingReader<R>
where
    R: AsyncRead + Send + Unpin,
{
    async fn simple_read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        {
            let address = &self as *const _;
            println!("{:?} => {:?}", address, std::thread::current().id());
        }
        self.inner.read(buf).await
    }
}

调用方恢复自然写法:

rust 复制代码
let n = file.simple_read(&mut buf[..]).await?;

此时配合 async_std::task::spawn 使用,利用多线程执行器并行哈希:

bash 复制代码
$ time ./target/release/surviving wine-* > /dev/null
./target/release/surviving wine-*  0.24s user 0.01s system 279% cpu 0.089 total

CPU 利用率 279%,明显超过 100%,说明真正在并行。


八、反向适配:从 SimpleReadAsyncRead

现在要解决一个反向问题:如果有一个类型实现了我们自定义的 SimpleRead,能否把它包装成实现 AsyncRead 的类型,从而传给其他需要 AsyncRead 的接口(比如 async_std::io::copy)?

8.1 第一次尝试:直接在 poll_read 里创建 Future

rust 复制代码
#[pin_project]
struct SimpleAsyncReader<R>
where
    R: SimpleRead,
{
    inner: R,
}

impl<R> AsyncRead for SimpleAsyncReader<R>
where
    R: SimpleRead,
{
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut [u8],
    ) -> Poll<io::Result<usize>> {
        let inner = self.project().inner;
        let mut fut = inner.simple_read(buf);
        Pin::new(&mut fut).poll(cx)  // poll 一次就丢掉
    }
}

这个版本在没有延迟的情况下能工作,因为 async_std::fs::File 的读取通常一次 poll 就完成。

但当我们给 TracingReader 加入人工延迟(用 futures-timer):

rust 复制代码
async fn simple_read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
    use futures_timer::Delay;
    use std::time::Duration;

    Delay::new(Duration::from_millis(50)).await;
    self.inner.read(buf).await
}

程序直接卡死了。

8.2 根本原因:Future 被 drop = 被取消

poll_read 每次被调用都会创建一个新的 Future,poll 一次,然后在函数返回时把这个 Future drop 掉。

一个 Future 被 drop,就意味着它被取消了。计时器订阅的唤醒事件也随之被注销,调度器永远不会再 poll poll_read,程序就卡死了。

这是 async Rust 中一个极其重要但容易被忽视的语义:Future 必须被持续 poll 直到 Poll::Ready,中途 drop 就是取消。

tracing 把执行过程可视化:

arduino 复制代码
poll_read{...}
  0ms  DEBUG polling future...
  simple_read{}
    0ms  DEBUG doing delay...
  0ms  DEBUG future was pending!
(程序再也不会被唤醒了)

确认了问题所在。

8.3 状态机方案:存储跨调用的 Future

解决方案:在结构体里维护一个 State,保存尚未完成的 Future,下次 poll_read 被调用时继续 poll 它:

rust 复制代码
enum State {
    Idle,
    Pending(Pin<Box<dyn Future<Output = io::Result<usize>> + Send>>),
}

但立即遇到生命周期问题:inner.simple_read(buf) 借用了 buf: &mut [u8],而 buf 的生命周期只覆盖当次 poll_read 调用。把持有 buf 引用的 Future 存储到跨调用的 State 里,生命周期不够长,编译器拒绝。

8.4 让 Future 持有 R 的所有权

如果 Future 不借用 buf,而是借用 innerR 类型),问题依然存在:inner 来自 self.project().inner,其生命周期只覆盖当次调用。

解决思路:让 Future 拥有 R 的所有权(通过 State::Idle(R) 存储),Future 结束时再把 R 返回。

rust 复制代码
enum State<R> {
    Idle(R),
    Pending(BoxFut<(R, io::Result<usize>)>),
    Transitional,  // swap 时的中间态
}

Transitional 是一个技巧------在用 std::mem::swap 取出状态时,先放一个占位值,避免留下"已移走但未初始化"的空洞。

buf: &mut [u8] 的问题仍未解决:Future 仍然需要借用 buf,而 buf 的生命周期不够长。

8.5 终极方案:让 SimpleAsyncReader 拥有自己的内部缓冲区

解决 buf 生命周期问题的唯一方式:让 SimpleAsyncReader 维护自己的内部 Vec<u8> 缓冲区,把它的所有权传给 Future,Future 完成后拿回来,再把数据从内部缓冲区复制到外部 buf 里。

最终的完整实现:

rust 复制代码
#[pin_project]
struct SimpleAsyncReader<R>
where
    R: SimpleRead,
{
    state: State<R>,
}

type BoxFut<T> = Pin<Box<dyn Future<Output = T> + Send>>;

enum State<R> {
    Idle(R, Vec<u8>),                              // 空闲:持有 reader 和内部缓冲
    Pending(BoxFut<(R, Vec<u8>, io::Result<usize>)>),  // 等待中:Future 拥有 reader 和缓冲
    Transitional,                                  // 状态切换时的占位
}

impl<R> AsyncRead for SimpleAsyncReader<R>
where
    R: SimpleRead + Send + 'static,  // 'static 因为 Future 是 'static 的
{
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut [u8],
    ) -> Poll<io::Result<usize>> {
        let proj = self.project();

        // 用 Transitional 占位,取出当前状态
        let mut state = State::Transitional;
        std::mem::swap(proj.state, &mut state);

        let mut fut = match state {
            State::Idle(mut inner, mut internal_buf) => {
                // 准备内部缓冲区,大小与外部 buf 相同
                internal_buf.clear();
                internal_buf.reserve(buf.len());
                unsafe { internal_buf.set_len(buf.len()) }

                // 创建 Future,inner 和 internal_buf 的所有权都转移进去
                Box::pin(async move {
                    let res = inner.simple_read(&mut internal_buf[..]).await;
                    (inner, internal_buf, res)
                })
            }
            State::Pending(fut) => {
                // 已有 Future,继续 poll
                fut
            }
            State::Transitional => unreachable!(),
        };

        match fut.as_mut().poll(cx) {
            Poll::Ready((inner, mut internal_buf, result)) => {
                // Future 完成:把数据从内部缓冲复制到外部 buf
                if let Ok(n) = &result {
                    let n = *n;
                    unsafe { internal_buf.set_len(n) }
                    buf[..n].copy_from_slice(&internal_buf[..]);
                } else {
                    unsafe { internal_buf.set_len(0) }
                }
                // 归还 inner 和 internal_buf,回到 Idle 状态
                *proj.state = State::Idle(inner, internal_buf);
                Poll::Ready(result)
            }
            Poll::Pending => {
                // Future 还在等待:保存 Future,下次继续 poll
                *proj.state = State::Pending(fut);
                Poll::Pending
            }
        }
    }
}

使用时:

rust 复制代码
async fn hash_file(path: &Path) -> Result<(), eyre::Error> {
    let file = File::open(path).await?;
    let file = TracingReader { inner: file };
    let mut file = SimpleAsyncReader {
        state: State::Idle(file, Default::default()),
    };

    let mut buf = vec![0u8; 256 * 1024];
    loop {
        let n = file.read(&mut buf[..]).await?;  // 使用标准 AsyncRead 接口
        match n {
            0 => break,
            n => hasher.update(&buf[..n]),
        }
    }
    // ...
}

九、验证:tracing 日志确认状态机正确工作

添加 tracing 工具链:

bash 复制代码
$ cargo add tracing tracing-futures tracing-tree tracing-subscriber

加上日志后的运行输出(简化):

arduino 复制代码
poll_read{...}
  0ms  DEBUG getting new future...
  simple_read{}
    0ms  DEBUG doing delay...
  0ms  DEBUG future was pending!
poll_read{...}
  0ms  DEBUG polling existing future...
  simple_read{}
    50ms DEBUG doing delay...done!
    50ms DEBUG doing read...
  0ms  DEBUG future was pending!
poll_read{...}
  0ms  DEBUG polling existing future...
  simple_read{}
    51ms DEBUG doing read...done!
  0ms  DEBUG future was ready!
poll_read{...}
  0ms  DEBUG getting new future...
  simple_read{}
    0ms  DEBUG doing delay...
  0ms  DEBUG future was pending!

状态转换清晰可见:

  1. 第一次调用 poll_readIdle → 创建 Future → poll → Pending(计时器未到)→ 存入 State::Pending
  2. 第二次调用:Pending → poll 已有 Future → 计时器到了,开始 read → Pending(read 未完成)
  3. 第三次调用:Pending → poll → read 完成 → Poll::Ready → 复制数据 → 回到 State::Idle
  4. 第四次调用:Idle → 下一个读取块,重复上面的流程

最终验证结果:

bash 复制代码
$ ./target/release/surviving ./wine-*
./wine-4.0.4.tar.xz d53a567e7a14a2b8dbd669afece1603daa769ada93522ec1f26fe69674d7c433
./wine-5.0.1.tar.xz 4af0d295e56db9f723daa4822ee4b4416ac8840278ccd7df73ef357027e5b663
./wine-5.0.2.tar.xz 2a48ac5363318b2b8dd222933002bac9fa0e1cc051605c17ebdae9f78ff03892

$ openssl dgst -sha3-256 wine-4.0.4.tar.xz
SHA3-256(wine-4.0.4.tar.xz)= d53a567e7a14a2b8dbd669afece1603daa769ada93522ec1f26fe69674d7c433

哈希值与 OpenSSL 完全一致,状态机正确工作。


十、总结:这次旅程教会了我们什么

关于 async 的本质

async 不是"让程序变快"的按钮,更准确地说,它是"让程序更能扩展"的按钮。单个 future 相比同步代码会有额外开销(更多的簿记工作),但执行器能够在一个线程池上同时推进数量远超线程数的 future,在高 I/O 并发场景下效果显著。

关于 Pin 的层次

  • Pin<&mut T> 保证 T 不会被移动
  • #[pin](pin-project)声明结构体字段的 pinning 是"结构性的"
  • 通过 self.project() 可以类型安全地获得 Pin<&mut F>(被 pin 的字段)或 &mut F(未被 pin 的字段)

关于 Send 的来源

Future 是否 Send 取决于它持有的所有值是否都是 Send 的。如果一个 Future 持有一个裸指针(*const T),它就不是 Send 的,哪怕这个指针只是临时用来打印地址。跨越 await 点的变量必须都是 Send 的。

关于 spawn vs spawn_local

  • spawn:在多线程工作窃取执行器上运行,要求 Future 是 Send + 'static
  • spawn_local:在单线程执行器上运行,不要求 Send,仍然支持协作式 I/O 并发

关于 Future 取消语义------最重要的一点

Drop 一个 Future 就是取消它。一旦 Future 没有被持续 poll 到 Poll::Ready,它注册的所有唤醒事件都会被撤销,后续调用永远不会发生。因此:

  • 如果你在 poll_read 里每次调用都创建新 Future,poll 一次就丢掉,一旦 Future 返回 Poll::Pending,程序就会永久卡死
  • 正确做法:把未完成的 Future 存入状态机,下次 poll_read 被调用时继续 poll 同一个 Future

关于 'static 约束的来源

Box<dyn Future + Send> 默认是 'static 的。这意味着存入状态机的 Future 不能借用任何外部数据------包括调用方传入的 buf: &mut [u8]。解决方案是让 Future 拥有自己的数据(通过所有权转移),必要时在完成后再复制回去。


附录:完整的依赖栈

crate 用途
color-eyre 带彩色 backtrace 的 Error 类型
argh 声明式命令行参数解析
sha3 SHA3-256 实现
async-std 异步运行时与 I/O
futures AsyncRead trait
pin-project 类型安全的 Pin 投影
async-trait Trait 中的 async fn(2020 年的临时方案)
futures-timer 异步计时器(用于演示延迟)
tracing 结构化日志与追踪

这些工具各司其职,在 async Rust 的生态中都有明确的定位。理解它们的工作原理,是真正驾驭 async Rust 的第一步。

相关推荐
倚栏听风雨4 小时前
Mac 本地开发:用 Nginx 配置自定义域名代理到本地服务
后端
菜菜小狗的学习笔记5 小时前
八股(九)杂七杂八
java·后端·spring
逍遥德5 小时前
Java编程高频的“技术点”-01:自定义全局异常处理器
java·开发语言·spring boot·后端
小旭95275 小时前
商品详情实现与缓存问题(穿透、击穿、雪崩)解决方案
java·数据库·spring boot·后端·缓存
迷渡6 小时前
用 Rust 重写的 Bun 有 13365 个 unsafe!
开发语言·后端·rust
AI_大白6 小时前
DeepSeek Function Calling 接入实时行情:从工具定义到多轮查询的完整示例
后端·架构
Cosolar6 小时前
从零搭建本地 RAG 系统:LangChain + LM Studio 完整实战指南
人工智能·后端·面试
mCell7 小时前
可观测性实战:Prometheus + Grafana 全栈监控
运维·后端·google
彭于晏Yan7 小时前
TransmittableThreadLocal原理及作用
spring boot·后端