如何将运行时创建的异步闭包存储在结构中?

Inf*_*Inf 2 rust

我正在学习 Rust 的异步/等待功能,并坚持执行以下任务。我想:

  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]);
}
Run Code Online (Sandbox Code Playgroud)

但由于生命周期无效,上述代码无法编译。

  1. 如何指定生命周期来编译上面的示例(这样 Rust 就会知道异步闭包应该与输入一样长)。据我了解,在提供的示例中,Rust 需要闭包才能具有静态生命周期?

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

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

谢谢

更新

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

egg*_*yal 5

    \n
  1. \n
    \n

    如何指定生命周期来编译上面的示例(这样 Rust 就会知道异步闭包应该与输入一样长)。据我了解,在提供的示例中,Rust 需要闭包才能具有静态生命周期?

    \n
    \n

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

    \n
        (s.s1)(&mut input).await;\n//  ^^^^^^^^^^^^^^^^^^\n//  closure invocation\n
    Run Code Online (Sandbox Code Playgroud)\n

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

    \n
        let future = (s.s1)(&mut input);\n\n    // do some other stuff\n\n    future.await;\n
    Run Code Online (Sandbox Code Playgroud)\n

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

    \n

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

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

    在操场上看到它

    \n

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

    \n
  2. \n
  3. \n
    \n

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

    \n
    \n

    让我们看一个极其简单的async块:

    \n
    async {\n    let mut x = 123;\n    let r = &mut x;\n    some_async_fn().await;\n    *r += 1;\n    x\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

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

    \n

    您可能已经观察到,未来还可以保存对存储在其他地方的事物的引用,例如&mut input@kmdreko\ 的答案;由于它们是借用的,因此在借用期间也不能移动它们。那么,为什么未来的不可移动性不能同样通过r借用x, 而无需固定呢?那么,未来的生命周期将取决于它的内容\xe2\x80\x94,而这种循环在 Rust 中是不可能的。

    \n

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

    \n
  4. \n
  5. \n
    \n

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

    \n
    \n

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

    \n

    解决方法,但使用装箱特征对象通常是最实用的解决方案\xe2\x80\x94,尽管它引入了堆分配和具有相应运行时成本的附加间接层。然而,这样的特征对象是不可避免的,其中结构的单个实例在其生命周期中Services可能持有不同的闭包,您从特征方法返回它们(当前可以\xe2\x80\x99t使用s1impl Trait,你从特征方法返回它们(目前可以\xe2\x80\x99t使用),或者在哪里您正在与一个不提供任何替代方案的库进行交互。

    \n
  6. \n
\n