引言
在 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.z
、stable
、beta
、nightly
等——与 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 的异步编程特性,同时避免潜在的性能问题和复杂性。