跳到主要内容

1 篇博文 含有标签「异步编程」

查看所有标签

引言

在 2023 年 12 月,一件小奇迹发生了:Traits 中的 async fn 正式发布。

自 Rust 1.39 版本起,我们已经拥有了独立的异步函数:

pub async fn read_hosts() -> eyre::Result<Vec<u8>> {
// 等等
}

以及在 impl 块中的异步函数:

impl HostReader {
pub async fn read_hosts(&self) -> eyre::Result<Vec<u8>> {
// 等等
}
}

但我们仍然无法在 Traits 中使用异步函数:

use std::io;

trait AsyncRead {
async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}

当尝试编译时,会出现如下错误:

❯ cargo +1.74.0 check --quiet
error[E0706]: functions in traits cannot be declared `async`
--> src/main.rs:9:5
|
9 | async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
| -----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| `async` because of this
|
= note: `async` trait functions are not currently supported
= note: consider using the `async-trait` crate: https://crates.io/crates/async-trait
= note: see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more information

For more information about this error, try `rustc --explain E0706`.
error: could not compile `sansioex` (bin "sansioex") due to previous error

Cool Bear 的热贴士

cargo +channel 语法有效

cargo +channel 语法有效,因为这里的 cargo 是由 rustup 提供的一个 shim。

有效的 channel 名称看起来像 x.y.zstablebetanightly 等——与 rust-toolchain.toml 文件或任何其他 toolchain override 中遇到的名称相同。

使用 async-trait crate

长期以来,推荐使用 async-trait crate 来在 Traits 中使用异步函数:

use std::io;

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

这种方法有效,但它改变了 Trait 定义(以及任何实现),使其返回已固定、盒装的 futures。

盒装 futures?

是的!这些是分配在堆上的 futures

为什么?

因为 futures——异步函数返回的值——可能具有不同的大小!

局部变量的大小

下面这个函数返回的 future 大小:

async fn foo() {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
println!("done");
}

与这个函数相比:

async fn bar() {
let mut a = [0u8; 72];
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
for _ in 0..10 {
a[0] += 1;
}
println!("done");
}

bar 函数的 future 大小更大,因为它包含了更多的状态需要跟踪:

❯ cargo run --quiet
foo: 128
bar: 200

因为 bar 函数中的数组在异步睡眠期间没有被释放——所有内容都存储在 future 中。

这就是一个问题,因为通常,当我们调用一个函数时,我们希望知道应该为返回值保留多少空间:我们说返回值是“有尺寸的”。

而在这里,“我们”实际上指的是编译器——为局部变量在栈上保留空间是函数被调用时的第一步之一。

在反汇编中可以看到:

❯ cargo asm sansioex::main --quiet --simplify --color | head -5

sansioex::main:
Lfunc_begin45:
sub sp, sp, #256
stp x20, x19, [sp, #224]
stp x29, x30, [sp, #240]

这里的 sub 指令总共保留了 256 字节。

Cool Bear 的热贴士

cargo asm 子命令

这里展示的 cargo asm 子命令来自 cargo-show-asm,可以通过以下命令安装:

cargo install --locked --all-features cargo-show-asm

原始的 cargo-asm crate 仍然有效,但功能较少,自 2018 年以来没有更新。

总结

在 Rust 中,Traits 中的异步函数的引入是一个重要的进展,但也带来了新的挑战, 如 async-trait crate 带来的 boxed futures 问题。 这一变化背后的原因主要是由于异步函数返回的 futures 大小不固定,编译器需要在编译时为函数返回值保留固定大小的空间。

通过理解这些技术细节,Rust 开发者可以更好地利用 Rust 的异步编程特性,同时避免潜在的性能问题和复杂性。

参考

鱼雪