nightly-09-11 引入的 async 块 `Send` 约束问题

原因及解决方案?

Posted by Hexi on November 7, 2019

本文旨在总结并解释 rust-lang/rust#64477 rust-lang/rust#64856。本文代码地址:Github

引言

如果你正在使用 2019-09-11 到 2019-11-05(最新)版本的 rust nightly 工具链,你会发现下面这段代码无法通过编译:

1
2
3
4
5
6
7
8
9
10
// examples/format.rs
async fn foo(_: String) {}

fn bar() -> impl Send {
    async move {
        foo(format!("")).await;
    }
}

fn main() {}

尝试运行:

1
cargo +nightly-2019-09-11 run --example format

编译报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
error[E0277]: `*mut (dyn std::ops::Fn() + 'static)` cannot be shared between threads safely
 --> examples/format.rs:3:13
  |
3 | fn bar() -> impl Send {
  |             ^^^^^^^^^ `*mut (dyn std::ops::Fn() + 'static)` cannot be shared between threads safely
  |
  = help: within `core::fmt::Void`, the trait `std::marker::Sync` is not implemented for `*mut (dyn std::ops::Fn() + 'static)`
  = note: required because it appears within the type `std::marker::PhantomData<*mut (dyn std::ops::Fn() + 'static)>`
  = note: required because it appears within the type `core::fmt::Void`
  = note: required because of the requirements on the impl of `std::marker::Send` for `&core::fmt::Void`
  = note: required because it appears within the type `std::fmt::ArgumentV1<'_>`
  = note: required because it appears within the type `[std::fmt::ArgumentV1<'_>; 0]`
  = note: required because it appears within the type `for<'r, 's, 't0, 't1, 't2, 't3, 't4, 't5, 't6, 't7, 't8, 't9, 't10, 't11, 't12, 't13> {fn(std::string::String) -> impl std::future::Future {foo}, for<'t14> fn(std::fmt::Arguments<'t14>) -> std::string::String {std::fmt::format}, fn(&'r [&'r str], &'r [std::fmt::ArgumentV1<'r>]) -> std::fmt::Arguments<'r> {std::fmt::Arguments::<'r>::new_v1}, [&'s str; 0], &'t0 [&'t1 str; 0], &'t2 [&'t3 str; 0], &'t4 [&'t5 str], (), [std::fmt::ArgumentV1<'t6>; 0], &'t7 [std::fmt::ArgumentV1<'t8>; 0], &'t9 [std::fmt::ArgumentV1<'t10>; 0], &'t11 [std::fmt::ArgumentV1<'t12>], std::fmt::Arguments<'t13>, std::string::String, impl std::future::Future}`
  = note: required because it appears within the type `[static generator@examples/format.rs:4:16: 6:6 for<'r, 's, 't0, 't1, 't2, 't3, 't4, 't5, 't6, 't7, 't8, 't9, 't10, 't11, 't12, 't13> {fn(std::string::String) -> impl std::future::Future {foo}, for<'t14> fn(std::fmt::Arguments<'t14>) -> std::string::String {std::fmt::format}, fn(&'r [&'r str], &'r [std::fmt::ArgumentV1<'r>]) -> std::fmt::Arguments<'r> {std::fmt::Arguments::<'r>::new_v1}, [&'s str; 0], &'t0 [&'t1 str; 0], &'t2 [&'t3 str; 0], &'t4 [&'t5 str], (), [std::fmt::ArgumentV1<'t6>; 0], &'t7 [std::fmt::ArgumentV1<'t8>; 0], &'t9 [std::fmt::ArgumentV1<'t10>; 0], &'t11 [std::fmt::ArgumentV1<'t12>], std::fmt::Arguments<'t13>, std::string::String, impl std::future::Future}]`
  = note: required because it appears within the type `std::future::GenFuture<[static generator@examples/format.rs:4:16: 6:6 for<'r, 's, 't0, 't1, 't2, 't3, 't4, 't5, 't6, 't7, 't8, 't9, 't10, 't11, 't12, 't13> {fn(std::string::String) -> impl std::future::Future {foo}, for<'t14> fn(std::fmt::Arguments<'t14>) -> std::string::String {std::fmt::format}, fn(&'r [&'r str], &'r [std::fmt::ArgumentV1<'r>]) -> std::fmt::Arguments<'r> {std::fmt::Arguments::<'r>::new_v1}, [&'s str; 0], &'t0 [&'t1 str; 0], &'t2 [&'t3 str; 0], &'t4 [&'t5 str], (), [std::fmt::ArgumentV1<'t6>; 0], &'t7 [std::fmt::ArgumentV1<'t8>; 0], &'t9 [std::fmt::ArgumentV1<'t10>; 0], &'t11 [std::fmt::ArgumentV1<'t12>], std::fmt::Arguments<'t13>, std::string::String, impl std::future::Future}]>`
  = note: required because it appears within the type `impl std::future::Future`
  = note: the return type of a function must have a statically known size

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: Could not compile `async-block-send`.

To learn more, run the command again with --verbose.

所以,这段代码到底有啥毛病呢?

多余的 Send 约束

让我们来看一段更简单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// examples/spurious-send.rs
use std::future::Future;
use std::pin::Pin;

fn f<T>(_: &T) -> Pin<Box<dyn Future<Output = ()> + Send>> {
    unimplemented!()
}

pub fn g<T: Sync>(x: &'static T) -> impl Future<Output = ()> + Send {
    async move { f(x).await }
}

fn main() {}

尝试运行:

1
cargo +nightly-2019-09-11 run --example spurious-send

编译报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
error[E0277]: `T` cannot be sent between threads safely
 --> examples/spurious-send.rs:8:37
  |
8 | pub fn g<T: Sync>(x: &'static T) -> impl Future<Output = ()> + Send {
  |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `T` cannot be sent between threads safely
  |
  = help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `T`
  = help: consider adding a `where T: std::marker::Send` bound
  = note: required because it appears within the type `for<'r, 's, 't0, 't1> {for<'t2> fn(&'t2 T) -> std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 'static)>> {f::<T>}, &'r T, T, &'s T, std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 't0)>>, std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 't1)>>, ()}`
  = note: required because it appears within the type `[static generator@examples/spurious-send.rs:9:16: 9:30 x:&T for<'r, 's, 't0, 't1> {for<'t2> fn(&'t2 T) -> std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 'static)>> {f::<T>}, &'r T, T, &'s T, std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 't0)>>, std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 't1)>>, ()}]`
  = note: required because it appears within the type `std::future::GenFuture<[static generator@examples/spurious-send.rs:9:16: 9:30 x:&T for<'r, 's, 't0, 't1> {for<'t2> fn(&'t2 T) -> std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 'static)>> {f::<T>}, &'r T, T, &'s T, std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 't0)>>, std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 't1)>>, ()}]>`
  = note: required because it appears within the type `impl std::future::Future`
  = note: the return type of a function must have a statically known size

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: Could not compile `async-block-send`.

To learn more, run the command again with --verbose.

这段报错信息令人匪夷所思。众所周知,如果 T: Sync,则有 &T: Send,所以这段代码应该是没问题的。T: Send 是不必要的,因为 async 块中不存在 T 类型的变量。

这个 bug 是 nightly-09-11 中引入的,并且已被 rust-lang/rust#64584 修复,所以最新的工具链是能够编译运行这段代码的:

1
cargo +nightly-2019-11-05 run --example spurious-send

不过,在含 .await 的语句中依然无法使用 format 宏。

format

1
2
3
4
5
6
7
8
9
10
// examples/format.rs
async fn foo(_: String) {}

fn bar() -> impl Send {
    async move {
        foo(format!("")).await;
    }
}

fn main() {}

format 宏会把 format("")展开成:

1
2
3
4
5
6
::alloc::fmt::format(::core::fmt::Arguments::new_v1(
            &[],
            &match () {
                () => [],
            },
        )))

这串表达式尝试了一些没有实现 Send 的临时变量,像 std::fmt::ArgumentV1, &core::fmt::Void 类型的变量。并且,这些临时变量会在当前语句结束之后才会被析构。

1
2
3
4
5
6
7
foo(::alloc::fmt::format(::core::fmt::Arguments::new_v1(
            &[],
            &match () {
                () => [],
            },
        )))).await;
^^^^^^^^^^^^^^^^^^^^^^ temporaries stay alive

所以,这些临时变量的生命期跨过了一次 await,这意味着它们会被作为无栈协程(由 async 块返回的 GenFuture)的一个字段存下来。

可以阅读这篇博客来了解有关 generator 的更多内容。

最终,这些没有实现 Send的临时变量会导致 GenFuture: !Send,这与函数返回类型 impl Send产生了冲突。

解决方案

  • 多余的 Send约束问题已被 rust-lang/rust#64584 解决,目前这个 PR 已经合入了 master 分区,并已经在最新的 nightly 工具链中生效。

  • rust-lang/rust#64856 提出了一份新的 format 实现,该 PR 目前合并进程被阻塞,但你可以手动实现一份来覆盖标准库实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  // examples/format-override.rs
  extern crate alloc;
  
  macro_rules! format {
      ($($arg:tt)*) => {{
          let res = alloc::fmt::format(alloc::__export::format_args!($($arg)*));
          res
      }}
  }
  
  async fn foo(_: String) {}
  
  fn bar() -> impl Send {
      async move {
          foo(format!("")).await;
      }
  }
  
fn main() {}

尝试运行:

1
  cargo +nightly-2019-11-05 run --example format-override

通过。