【问题标题】:What's the difference of lifetime inference between async fn and async closure?async fn 和 async 闭包之间的生命周期推断有什么区别?
【发布时间】:2020-06-30 05:32:36
【问题描述】:

看看这段代码:

#![feature(async_closure)]

use std::future::Future;
use std::pin::Pin;

trait A<'a> {
    fn call(&'a self, data: &'a i32) -> Pin<Box<dyn 'a + Future<Output=()>>>;
}

impl <'a, F, Fut> A<'a> for F
where Fut: 'a + Future<Output=()>,
      F: Fn(&'a i32) -> Fut
{
    fn call(&'a self, data: &'a i32) -> Pin<Box<dyn 'a + Future<Output=()>>> {
        Box::pin(self(data))
    }
}

async fn sample(_data: &i32) {

}

fn is_a(_: impl for<'a> A<'a>) {

}

fn main() {
    is_a(sample);
    is_a(async move |data: &i32| {
        println!("data: {}", data);
    });
}

Playground

为什么is_a(sample) 有效但下一行编译失败? async fn 和 async 闭包的生命周期推断有什么区别?

闭包版本失败并出现以下错误:

error: implementation of `A` is not general enough
  --> src/main.rs:29:5
   |
6  | / trait A<'a> {
7  | |     fn call(&'a self, data: &'a i32) -> Pin<Box<dyn 'a + Future<Output=()>>>;
8  | | }
   | |_- trait `A` defined here
...
29 |       is_a(async move |data: &i32| {
   |       ^^^^ implementation of `A` is not general enough
   |
   = note: `A<'1>` would have to be implemented for the type `[closure@src/main.rs:29:10: 31:6]`, for any lifetime `'1`...
   = note: ...but `A<'_>` is actually implemented for the type `[closure@src/main.rs:29:10: 31:6]`, for some specific lifetime `'2`

【问题讨论】:

    标签: rust async-await closures lifetime


    【解决方案1】:

    async || 闭包的返回类型是编译器生成的匿名类型。

    这种类型实现了Future,它捕获了一个额外的 生命周期与 async || 闭包的范围有关。

    fn main() {
    
        let closure = async move |data: &i32| {   --+ '2 start
            println!("data: {}", data);             |
        };                                          |
                                                    |
        is_a(closure);                              |
                                                    v 
    }
    

    异步块返回一个带有如下签名的类型:

    impl Future<Output = SomeType> + '2 + '...
    

    '2 是闭包的生命周期。

    请注意,当使用异步函数而不是闭包时,没有这个额外的生命周期要求。

    当您像这样拨打is_a 时:

    let closure = async move |data: &i32| {
        println!("data: {}", data);
    };
    
    is_a(closure);
    

    你得到:

    error: implementation of `A` is not general enough
    ...
    = note: `A<'1>` would have to be implemented for the type `[closure@src/main.rs:43:19: 45:6]`, for any lifetime `'1`...
    = note: ...but `A<'_>` is actually implemented for the type `[closure@src/main.rs:43:19: 45:6]`, for some specific lifetime `'2`
    

    因为closure 参数是为特定生命周期'2 实现的类型,但需要任何生命周期:

    fn is_a(_: impl for<'a> A<'a>) {}
    

    请注意,您注意到的错误确实隐藏了另一个源自捕获的 '2 生命周期的生命周期违规。

    为:

    let closure = async move |data: &i32| {
        println!("data: {}", data);
    };
    

    编译器报告:

    error: lifetime may not live long enough
      --> src/main.rs:43:19
       |
    43 |     let closure = async move |data: &i32| {
       |                   ^^^^^^^^^^^^^^^^^^-^^^-
       |                   |                 |   |
       |                   |                 |   return type of closure is impl std::future::Future
       |                   |                 let's call the lifetime of this reference `'1`
       |                   returning this value requires that `'1` must outlive `'2`
    

    这让我得出结论,不可能在 async || 闭包内使用参数引用。

    为什么需要生命周期'2?

    生命周期概念是关于内存安全的,它归结为保证参考 指向内存插槽就是指向一个有效值。

    fn my_function() {
    
        let value = 1                           --+ '1 start           
                                                  |
        let closure = async move |data: &i32| {   |       --+ '2 start
            println!("data: {}", data);           |         |
        };                                        |         |
                                                  |         |
        tokio::spawn(closure(&value))             |         |
                                                 -+ '1 end  |
    }                                                       v continue until   
                                                              the future complete
    

    考虑上面的例子:value 是分配在栈上的一个内存槽,它一直有效直到 my_function 返回并展开堆栈。

    '1 的生命周期考虑了value 的有效期,当my_function 返回时 引用 &amp;value 不再有效。

    但是'2的生命从何而来?

    这是因为closure(&amp;value) 返回一个实体,该实体实现了一个Future,它将存在于运行时执行器中, 在这种情况下是 tokio 执行器,直到计算结束。

    '2 生命周期将考虑Future 的这个有效范围。

    为了说明'2 终身必要性的原因,请考虑以下情况:

    fn run_asyn_closure() {
        let data: i32 = 1;
    
        let closure = async move |data: &i32| {
            println!("starting task with data {}", data);
    
            // yield the computation for 3 seconds, awaiting for completion of long_running_task
            long_running_task().await;
    
            // data points to a memory slot on the stack that meantime is rewritten
            // because run_asyn_closure returned 3 seconds ago
            println!("using again data: {}", data); // BANG!! data is not more valid
        };
    
        tokio::spawn(closure(&data));
    }
    

    请注意,实际上tokio::spawn 需要&amp;data 引用具有'static 生命周期, 但这与理解这个主题无关。

    【讨论】:

    • rfc1558 的异步版本是必需的。
    • 我明白你的意思,但据我所知,这可能会被 rust 的内存安全规则所禁止。在异步函数/块后面有一个状态机需要跟踪完成进度,该完成将在未来某个时间点发生。尝试想象一个(被 rust 禁止)场景,其中 data: &amp;i32 引用最初存储到状态机中,之后它指向的值被删除(data 变得无效),并且在未来的某个时间更远的状态机器取得一些进展并尝试使用无效的data 引用...
    • GenFuture 的生命周期依赖于 data 的生命周期是合理的。但是为什么它必须依赖于闭包的生命周期呢?
    • 因为闭包(或者在这种情况下最好说 closure())评估为一个 Future ,其生命周期可能比 data 长,编译器会注意到这一点。
    • 令人困惑...据我了解,GenFuture 没有捕获来自closure() 的任何引用,它仅包含参数引用data。在这种情况下,未来的生命周期不应该依赖于closure(),不是吗?未来是否包含对closure() 的任何引用?
    猜你喜欢
    • 2018-12-05
    • 2020-12-11
    • 2015-01-06
    • 2019-08-19
    • 2020-09-16
    • 1970-01-01
    • 2013-08-11
    • 1970-01-01
    相关资源
    最近更新 更多