改进 async-await 的“Future 未实现 Send”诊断信息

2019 年 10 月 11 日 · David Wood 代表 异步基础工作组

Async-await 将在 1.39 版本中进入稳定版(只剩一个月了!),正如上月在《Async Foundations Update: Time for polish!》(异步基础更新:精雕细琢的时候到了!)帖子中宣布的那样,异步基础工作组(Async Foundations WG)已将重心转移到完善工作上。本文将重点介绍这一工作的一个方面:诊断信息的改进,特别是工作组一直在努力改进的、曾经不太有帮助的“future 未实现 Send”诊断信息。

为什么我的 future 没有实现 Send

Async-await 的一个主要优势在于多线程上下文,在这些场景下,能够将 future 发送到其他线程非常有用。这可能看起来像下面这样(为简洁起见,这里没有实际创建线程,只是要求 future 实现 std::marker::Send 特性)

use std::sync::{Mutex, MutexGuard};

fn is_send<T: Send>(t: T) { }

async fn foo() {
    bar(&Mutex::new(22)).await
}

async fn bar(x: &Mutex<u32>) {
    let g = x.lock().unwrap();
    baz().await
}

async fn baz() { }

fn main() {
    is_send(foo());
}

当我们尝试编译这段代码时,会得到一个冗长且难以理解的诊断信息:

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) {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^

这……不太好。让我们来分解一下发生的情况,并理解这个错误试图告诉我们什么。

fn main() {
    is_send(foo());
}

main 函数中,我们调用了 foo 并将其返回值传递给了 is_sendfoo 是一个 async fn,所以它不像没有指定返回类型的函数那样返回 ()。相反,它返回 impl std::future::Future<Output = ()>,这是一个实现了 std::future::Future 的匿名类型。

async fn foo() {
    bar(&Mutex::new(22)).await
}

// becomes...

fn foo() -> impl std::future::Future<Output = ()> {
    async move {
        bar(&Mutex::new(22)).await
    }
}

如果您想了解更多关于 async-await 发生的转换,可以阅读 async book 中的 async/.await 入门章节

fn is_send<T: Send>(t: T) { }

看起来我们得到的错误是因为 foo 返回的 future 没有满足 is_send 函数的 T: Send 约束。

Async 函数是如何实现的?

为了解释为什么我们的 future 没有实现 Send,我们首先需要稍微了解一下 async-await 在底层是如何工作的。rustc 使用生成器(generators)来实现 async fn,这是一种用于可恢复函数(类似于您可能在其他语言中熟悉的协程)的不稳定语言特性。生成器像枚举一样布局,其变体包含所有在 await 点(它被 desugar 为生成器 yield)之间使用的变量。

async fn bar(x: &Mutex<u32>) {
    let g = x.lock().unwrap();
    baz().await // <- await point (suspend #0), `g` and `x` are in use before await point
} // <- `g` and `x` dropped here, after await point
enum BarGenerator {
    // `bar`'s parameters.
    Unresumed { x: &Mutex<u32> },

    Suspend0 {
        // Future returned by `baz`, which is being polled.
        baz_future: BazGenerator,

        // Locals that are used across the await point.
        x: &Mutex<u32>,
        g: MutexGuard<'_, u32>,
    },

    Returned { value: () }
}

如果您想了解更多关于 async fn 实现细节的信息,Tyler Mandry 撰写了一篇非常出色的博客文章,更深入地探讨了这方面的工作,绝对值得一读。

那么,为什么我的 future 没有实现 Send

我们现在知道 async fn 在后台被表示为一个枚举。在同步 Rust 中,您可能习惯于当编译器认为合适时,您的类型会自动实现 Send——通常是当您的类型的所有字段都实现了 Send 时。因此,代表我们 async fn 的类似枚举的类型,如果其中所有类型都实现了 Send,那么它也应该实现 Send

换句话说,如果所有在 .await 点之间持有的类型都实现了 Send,那么一个 future 就可以安全地跨线程发送。这种行为非常有用,因为它让我们能够编写与 async-await 平滑互操作的泛型代码,但如果没有诊断支持,我们会得到令人困惑的错误消息。

那么,示例中哪个类型有问题呢?

回到我们的示例,这个 future 一定在某个 .await 点之间持有一个没有实现 Send 的类型,但问题在哪里呢?这正是诊断改进旨在帮助回答的主要问题。让我们先看看 foo 函数:

async fn foo() {
    bar(&Mutex::new(22)).await
}

foo 调用了 bar,传递了一个指向 std::sync::Mutex 的引用并得到一个 future,然后在它上面执行 await

async fn bar(x: &Mutex<u32>) {
    let g: MutexGuard<u32> = x.lock().unwrap();
    baz().await
} // <- `g` is dropped here

barawait 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 {
    kind: DerivedObligation(/* generator that captured `g` */),
    source: /* `Span` type pointing at `bar`'s location in user code */,
    parent: Some(Obligation {
        kind: DerivedObligation(/* generator calling `bar` */),
        source: /* `Span` type pointing at `foo`'s location in user code */,
        parent: Some(Obligation {
            kind: ItemObligation(/* type representing `std::marker::Send` */),
            source: /* `Span` type pointing at `is_send`'s location in user code */,
            cause: None,
        }),
    }),
}

编译器匹配这个链,期望找到一个 ItemObligation 和一些包含生成器的 DerivedObligation,这标识了我们要改进的错误。利用这些义务中的信息,rustc 可以构造上面所示的专门化错误——如果您想看看实际的实现是什么样的,请查看 PR #64895

如果您有兴趣改进此类诊断信息,或者只是想修复 bug,请考虑为编译器贡献!有很多工作组可以加入,也有资源可以帮助您入门(例如 rustc 开发指南编译器团队文档)。

接下来是什么?

计划并正在进行更多改进,以便该诊断信息适用于更多情况,并为 SendSync 提供专门的消息,如下所示:

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