首页 > 解决方案 > 如何将运行时创建的异步闭包存储在结构中?

问题描述

我正在学习 Rust 的 async/await 功能,并坚持以下任务。我想:

  1. 在运行时创建一个异步闭包(或者更好地说是异步块);
  2. 将创建的闭包传递给某个结构的构造函数并存储它;
  3. 稍后执行创建的闭包。

查看类似的问题,我编写了以下代码:

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

struct Services {
    s1: Box<dyn FnOnce(&mut Vec<usize>) -> Pin<Box<dyn Future<Output = ()>>>>,
}

impl Services {
    fn new(f: Box<dyn FnOnce(&mut Vec<usize>) -> Pin<Box<dyn Future<Output = ()>>>>) -> Self {
        Services { s1: f }
    }
}

enum NumberOperation {
    AddOne,
    MinusOne
}

#[tokio::main]
async fn main() {
    let mut input = vec![1,2,3];
    let op = NumberOperation::AddOne;
    
    let s = Services::new(Box::new(|numbers: &mut Vec<usize>| Box::pin(async move {
        for n in numbers {
            match op {
                NumberOperation::AddOne => *n = *n + 1,
                NumberOperation::MinusOne => *n = *n - 1,
            };
        }
    })));

    (s.s1)(&mut input).await;
    assert_eq!(input, vec![2,3,4]);
}

但是上面的代码不会编译,因为无效的生命周期。

  1. 如何指定生命周期以使上述示例编译(这样 Rust 将知道异步闭包应该与输入一样长)。正如我在提供的示例中所理解的那样,Rust 需要闭包才能拥有静态生命周期?

  2. 另外还不清楚为什么我们必须使用 Pin<Box> 作为返回类型?

  3. 是否有可能以某种方式重构代码并消除:Box::new(|arg: T| Box::pin(async move {}))?也许有一些板条箱?

谢谢

更新

有类似的问题如何将异步函数存储在结构中并从结构实例中调用它? . 尽管这是一个类似的问题,实际上我的示例是基于该问题的答案之一。第二个答案包含有关在运行时创建的闭包的信息,但似乎它仅在我传递一个拥有的变量时才有效,但在我的示例中,我想传递给在运行时可变引用创建的闭包,而不是拥有的变量。

标签: rust

解决方案


  1. 如何指定生命周期以使上述示例编译(这样 Rust 将知道异步闭包应该与输入一样长)。正如我在提供的示例中所理解的那样,Rust 需要闭包才能拥有静态生命周期?

    让我们仔细看看调用闭包时会发生什么:

        (s.s1)(&mut input).await;
    //  ^^^^^^^^^^^^^^^^^^
    //  closure invocation
    

    关闭立即返回一个未来。您可以将该未来分配给一个变量并保留它直到以后:

        let future = (s.s1)(&mut input);
    
        // do some other stuff
    
        future.await;
    

    问题是,由于未来是封闭的,它可能会在程序的余生中一直存在,而不会被驱赶到完成;也就是说,它可以有'static生命周期。并且 input显然必须继续借用直到未来解决:否则想象一下,例如,如果上面的“其他一些东西”涉及修改、移动甚至丢弃input会发生什么——想想未来运行时会发生什么?

    一种解决方案是将所有权传递Vec给闭包,然后从未来再次返回:

        let s = Services::new(Box::new(move |mut numbers| Box::pin(async move {
            for n in &mut numbers {
                match op {
                    NumberOperation::AddOne => *n = *n + 1,
                    NumberOperation::MinusOne => *n = *n - 1,
                };
            }
            numbers
        })));
    
        let output = (s.s1)(input).await;
        assert_eq!(output, vec![2,3,4]);
    

    在操场上看到它

    @kmdreko 的回答显示了您实际上如何将借用的生命周期与返回的未来的生命周期联系起来。

  2. 也不清楚为什么我们必须使用 Pin 作为返回类型?

    让我们看一个愚蠢的简单async块:

    async {
        let mut x = 123;
        let r = &mut x;
        some_async_fn().await;
        *r += 1;
        x
    }
    

    请注意,执行可能会在await. x发生这种情况时, and的现有值r必须临时存储(在Future对象中:它只是一个结构,在这种情况下具有字段 forxr)。但是r是对同一结构中另一个字段的引用!如果未来从其当前位置移动到内存中的其他位置,r仍将指的是旧位置x而不是新位置。未定义的行为。坏坏坏。

    您可能已经观察到,future 还可以引用存储在其他地方的事物,例如&mut input@kmdreko 的答案;因为它们是借来的,所以在借用期间也不能移动。那么,为什么未来的不可移动性同样不能通过r' 的借用而x不是钉住来强制执行?那么,未来的生命周期将取决于它的内容——而这种循环在 Rust 中是不可能的。

    这通常是自引用数据结构的问题。Rust 的解决方案是防止它们被移动:即“固定”它们。

  3. 是否有可能以某种方式重构代码并消除:Box::new(|arg: T| Box::pin(async move {}))?也许有一些板条箱?

    在您的具体示例中,闭包和未来可以驻留在堆栈上,您可以简单地摆脱所有装箱和固定(借用检查器可以确保堆栈项在没有明确固定的情况下不会移动)。然而,如果你想Services从一个函数返回,你会遇到说明它的类型参数的困难:impl Trait通常是你解决这类问题的首选解决方案,但它是有限的,并且(当前)没有扩展到关联类型,例如返回的未来。

    有一些变通方法,但使用盒装 trait 对象通常是最实用的解决方案——尽管它引入了堆分配和额外的间接层以及相应的运行时成本。然而,这样的 trait 对象是不可避免的,其中结构的单个实例Services可能在其生命周期中持有不同的闭包s1,您从 trait 方法(当前无法使用impl Trait)返回它们,或者您正在与一个不提供任何替代方案的库。


推荐阅读