Async-await 将在 1.39 版本中进入稳定版(只剩一个月了!),正如上月在《Async Foundations Update: Time for polish!》(异步基础更新:精雕细琢的时候到了!)帖子中宣布的那样,异步基础工作组(Async Foundations WG)已将重心转移到完善工作上。本文将重点介绍这一工作的一个方面:诊断信息的改进,特别是工作组一直在努力改进的、曾经不太有帮助的“future 未实现 Send”诊断信息。
Send
?
为什么我的 future 没有实现 Async-await 的一个主要优势在于多线程上下文,在这些场景下,能够将 future 发送到其他线程非常有用。这可能看起来像下面这样(为简洁起见,这里没有实际创建线程,只是要求 future 实现 std::marker::Send
特性)
use ;
async
async
async
当我们尝试编译这段代码时,会得到一个冗长且难以理解的诊断信息:
error[E0277]: `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
--> src/main.rs:23:5
|
23 | is_send(foo());
| ^^^^^^^ `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
|
= help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
= note: required because it appears within the type `for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}`
= note: required because it appears within the type `[static generator@src/main.rs:13:30: 16:2 x:&std::sync::Mutex<u32> for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}]`
= note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:13:30: 16:2 x:&std::sync::Mutex<u32> for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}]>`
= note: required because it appears within the type `impl std::future::Future`
= note: required because it appears within the type `impl std::future::Future`
= note: required because it appears within the type `for<'r> {impl std::future::Future, ()}`
= note: required because it appears within the type `[static generator@src/main.rs:9:16: 11:2 for<'r> {impl std::future::Future, ()}]`
= note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:9:16: 11:2 for<'r> {impl std::future::Future, ()}]>`
= note: required because it appears within the type `impl std::future::Future`
= note: required because it appears within the type `impl std::future::Future`
note: required by `is_send`
--> src/main.rs:5:1
|
5 | fn is_send<T: Send>(t: T) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^
这……不太好。让我们来分解一下发生的情况,并理解这个错误试图告诉我们什么。
在 main
函数中,我们调用了 foo
并将其返回值传递给了 is_send
。foo
是一个 async fn
,所以它不像没有指定返回类型的函数那样返回 ()
。相反,它返回 impl std::future::Future<Output = ()>
,这是一个实现了 std::future::Future
的匿名类型。
async
// becomes...
如果您想了解更多关于 async-await 发生的转换,可以阅读 async book 中的 async
/.await
入门章节。
看起来我们得到的错误是因为 foo
返回的 future 没有满足 is_send
函数的 T: Send
约束。
Async 函数是如何实现的?
为了解释为什么我们的 future 没有实现 Send
,我们首先需要稍微了解一下 async-await 在底层是如何工作的。rustc 使用生成器(generators)来实现 async fn
,这是一种用于可恢复函数(类似于您可能在其他语言中熟悉的协程)的不稳定语言特性。生成器像枚举一样布局,其变体包含所有在 await 点(它被 desugar 为生成器 yield)之间使用的变量。
async // <- `g` and `x` dropped here, after await point
如果您想了解更多关于 async fn
实现细节的信息,Tyler Mandry 撰写了一篇非常出色的博客文章,更深入地探讨了这方面的工作,绝对值得一读。
Send
?
那么,为什么我的 future 没有实现 我们现在知道 async fn
在后台被表示为一个枚举。在同步 Rust 中,您可能习惯于当编译器认为合适时,您的类型会自动实现 Send
——通常是当您的类型的所有字段都实现了 Send
时。因此,代表我们 async fn
的类似枚举的类型,如果其中所有类型都实现了 Send
,那么它也应该实现 Send
。
换句话说,如果所有在 .await
点之间持有的类型都实现了 Send
,那么一个 future 就可以安全地跨线程发送。这种行为非常有用,因为它让我们能够编写与 async-await 平滑互操作的泛型代码,但如果没有诊断支持,我们会得到令人困惑的错误消息。
那么,示例中哪个类型有问题呢?
回到我们的示例,这个 future 一定在某个 .await
点之间持有一个没有实现 Send
的类型,但问题在哪里呢?这正是诊断改进旨在帮助回答的主要问题。让我们先看看 foo
函数:
async
foo
调用了 bar
,传递了一个指向 std::sync::Mutex
的引用并得到一个 future,然后在它上面执行 await
。
async // <- `g` is dropped here
bar
在 await
baz
之前锁定了互斥锁。std::sync::MutexGuard<u32>
没有实现 Send
,并且它跨越了 baz().await
点(因为 g
在作用域结束时才被丢弃),这导致整个 future 没有实现 Send
。
这在错误信息中并不明显:我们需要知道 future 是否实现 Send
取决于它们捕获的类型,并且需要自己找到跨越 await 点的类型。
幸运的是,异步基础工作组一直在努力改进这个错误,并且在 nightly 版本中,我们看到了以下改进后的诊断信息:
error[E0277]: `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
--> src/main.rs:23:5
|
5 | fn is_send<T: Send>(t: T) {
| ------- ---- required by this bound in `is_send`
...
23 | is_send(foo());
| ^^^^^^^ `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
|
= help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
note: future does not implement `std::marker::Send` as this value is used across an await
--> src/main.rs:15:3
|
14 | let g = x.lock().unwrap();
| - has type `std::sync::MutexGuard<'_, u32>`
15 | baz().await;
| ^^^^^^^^^^^ await occurs here, with `g` maybe used later
16 | }
| - `g` is later dropped here
好多了!
它是如何工作的?
当 rustc 的特性系统确定一个特性没有实现(在本例中是 std::marker::Send
)时,它会发出这个错误。特性系统会产生一个“义务”(obligations)链。义务是表示约束(例如 is_send
中的 T: Send
)起源地或约束传播位置的类型。
为了改进这个诊断信息,义务链现在被视为堆栈帧,其中每个义务的“帧”代表每个函数对错误的贡献。让我们用一个非常粗略的近似来具体说明:
Obligation
编译器匹配这个链,期望找到一个 ItemObligation
和一些包含生成器的 DerivedObligation
,这标识了我们要改进的错误。利用这些义务中的信息,rustc 可以构造上面所示的专门化错误——如果您想看看实际的实现是什么样的,请查看 PR #64895。
如果您有兴趣改进此类诊断信息,或者只是想修复 bug,请考虑为编译器贡献!有很多工作组可以加入,也有资源可以帮助您入门(例如 rustc 开发指南 或 编译器团队文档)。
接下来是什么?
计划并正在进行更多改进,以便该诊断信息适用于更多情况,并为 Send
和 Sync
提供专门的消息,如下所示:
error[E0277]: future cannot be sent between threads safely
--> src/main.rs:23:5
|
5 | fn is_send<T: Send>(t: T) {
| ------- ---- required by this bound in `is_send`
...
23 | is_send(foo());
| ^^^^^^^ future returned by `foo` is not `Send`
|
= help: future is not `Send` as this value is used across an await
note: future does not implement `std::marker::Send` as this value is used across an await
--> src/main.rs:15:3
|
14 | let g = x.lock().unwrap();
| - has type `std::sync::MutexGuard<'_, u32>`
15 | baz().await;
| ^^^^^^^^^^^ await occurs here, with `g` maybe used later
16 | }
| - `g` is later dropped here