2022 年 1 月 26 日 - 4 分钟
用吸引点击的标题引起注意后,解释其含义。当前 Rust 代码可能因对 async Rust 代码的某些假设而以非常微妙的方式出错,这些假设不一定总是正确的。
故事始于最近在sentry-rust
中修复的一个 bug(表现为内存泄漏),该 bug 突出了问题的两个方面。
问题的根本原因并非特定于 Sentry SDK,在其他情况下也很容易发生。将使用tracing
生态系统中的示例,因为其用户基础可能比 Sentry 更广泛。
问题归结为async fn X()
和fn X() -> impl Future
之间非常微妙的差异,以及第二个不保证完全惰性的事实。
async fn
按定义是惰性的。通过async_fn()
调用它只会将参数捕获到一个匿名Future
类型中,但在其他方面保证不会立即执行函数体,而是在poll
时执行。对于返回impl Future
的函数以及返回(泛型或命名)期货的特性函数则不是这样。这些函数在调用时和poll 时可能有不同数量的代码运行。
fn fully_lazy() -> impl Future<Output = ()> {
async move {
println!("print happens on *poll*");
}
}
fn not_lazy() -> impl Future<Output = ()> {
println!("print happens on *call*");
std::future::ready(())
}
如果可能,将所有代码移到 async 块中,但这并不总是可行的。
[](#aside-named-types) 旁白:命名类型
目前存在async fn
返回匿名类型的问题,返回impl Future
也有同样的问题。在稳定的 Rust 中,今天无法命名该类型。当希望将未来放在struct
或集合中时,需要命名类型。一般来说,为一个箱子的每个公共 API 类型命名也是一种良好的实践。
同样的原则也是每个Iterator
组合器返回其自己的命名类型的原因,也是存在[tracing::instrument::Instrumented](https://docs.rs/tracing/latest/tracing/instrument/struct.Instrumented.html)
和[sentry::SentryFuture](https://docs.rs/sentry/latest/sentry/struct.SentryFuture.html)
的原因。
夜间 Rust 提供了[type_alias_impl_trait](https://github.com/rust-lang/rust/issues/63063)
特性,允许为其他匿名期货命名。不幸的是,由于是夜间特性,在稳定的 Rust 中尚未可用。
在稳定的 Rust 上我们可以使用的下一个最好的东西是Pin<Box<dyn Future>>
,如下所示:
fn dyn_future() -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(async move {
println!("print happens on *poll*");
})
}
这也是async-trait
箱子所做的去糖操作(嗯几乎)。生成的类型有一个名称,我们可以将其放入struct
和集合中。然而,它有堆分配和动态调度的缺点,并且在#![no_std]
构建中也不可用。
因此,更广泛的社区似乎已经决定创建自己的Future
类型,这些类型通常通过泛型包装内部期货。这本身就带来了很多不便,因为手动实现Future::poll
是一场噩梦,通常需要手动unsafe
代码,或者引入一个外部依赖pin-project。
出于这个原因,我采用了以下模式,我打赌很多其他人也这样做:
fn returns_named_future() -> NamedFuture {
// 尽可能多地进行逻辑而无需使用`await`
// 返回结果期货,在`poll`内部完成其余操作
NamedFuture
}
[](#problematic-tracing-example) 有问题的tracing
示例
为了进一步说明问题,用一个完整的实际tracing
示例来演示两个问题,该示例遵循[tracing::Span
文档](https://docs.rs/tracing/lates...中的指南:
use std::future::Future;
use tracing::Instrument;
fn fully_lazy() -> impl Future<Output = ()> {
async move {
tracing::info!("log happens on *poll*");
}
}
fn not_lazy() -> impl Future<Output = ()> {
tracing::info!("log happens on *call*");
std::future::ready(())
}
fn broken_parent_lazy() -> impl Future<Output = ()> {
let span = tracing::info_span!("broken_parent");
fully_lazy().instrument(span)
}
fn broken_parent_not_lazy() -> impl Future<Output = ()> {
let span = tracing::info_span!("broken_parent");
not_lazy().instrument(span)
}
fn correct_parent_lazy() -> impl Future<Output = ()> {
let span = tracing::info_span!("correct_parent");
span.in_scope(|| fully_lazy()).instrument(span)
}
fn correct_parent_not_lazy() -> impl Future<Output = ()> {
let span = tracing::info_span!("correct_parent");
span.in_scope(|| not_lazy()).instrument(span)
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
broken_parent_lazy().await;
broken_parent_not_lazy().await;
correct_parent_lazy().await;
correct_parent_not_lazy().await;
}
运行此示例输出以下内容:
2022-01-26T14:48:11.115115Z INFO broken_parent: lazy_futures: log happens on *poll*
2022-01-26T14:48:11.115511Z INFO lazy_futures: log happens on *call*
2022-01-26T14:48:11.115732Z INFO correct_parent: lazy_futures: log happens on *poll*
2022-01-26T14:48:11.116020Z INFO correct_parent: lazy_futures: log happens on *call*
可以看到,第二行日志(来自broken_parent_not_lazy().await
)没有在预期的正确范围内运行。其他示例按预期工作,要么调用者处理在调用中执行代码的不太可能的情况,要么被调用者更正确地不在调用中执行任何代码。
[](#conclusion) 结论
这里有两个建议,涵盖了问题的两个方面:
- 不要对第三方代码是惰性的做出假设。外部代码可能以在调用与poll中执行实际代码的方式“行为不端”。
- 使所有代码完全惰性。不要在调用与poll中进行任何计算。外部代码可能依赖于期货代码是完全惰性的,否则可能会违反预期。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。