本文是对 Surviving Rust async interfaces 的整理与翻译
我曾经很害怕 async Rust。很容易掉坑里。
但随着社区的持续努力,async Rust 每周都在变得更容易使用。这篇文章从一个具体的任务出发------计算文件的 SHA3-256 哈希值------从同步实现开始,一步一步引入异步,踩遍所有该踩的坑,最终手工实现一个正确的异步状态机适配器。
涉及的核心问题包括:Pin 和 Unpin 到底在做什么、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 点可以被调度到任意线程继续执行。
五、实现自定义 AsyncRead:Pin 的本质
为了进一步理解调度行为,我们想实现一个"追踪读取器"------在每次 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 self:Pin保证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_mut 和 map_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 的语义是:我保证字段 inner 在 self 被 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).await(AsyncRead 系列方法需要 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%,说明真正在并行。
八、反向适配:从 SimpleRead 到 AsyncRead
现在要解决一个反向问题:如果有一个类型实现了我们自定义的 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,而是借用 inner(R 类型),问题依然存在: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!
状态转换清晰可见:
- 第一次调用
poll_read:Idle→ 创建 Future → poll → Pending(计时器未到)→ 存入State::Pending - 第二次调用:
Pending→ poll 已有 Future → 计时器到了,开始 read → Pending(read 未完成) - 第三次调用:
Pending→ poll → read 完成 →Poll::Ready→ 复制数据 → 回到State::Idle - 第四次调用:
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 + 'staticspawn_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 的第一步。