如何正确地从Rust中Hyper的异步闭包中的外部范围读取字符串值

ybzsozfc  于 2023-05-29  发布在  其他
关注(0)|答案(1)|浏览(193)

bounty还有16小时到期。回答此问题可获得+100声望奖励。Tim Perry希望引起更多注意这个问题:

我真的很感激一个“正确”的方式来做到这一点的例子,为什么。
我正在尝试学习Rust,并尝试编写一些非常简单的Web服务器代码来实现这一点。
我以为我对生命周期的基本知识有了一个很好的了解&借用简单的代码,但我发现我在某个地方缺少了一个基本的技术,或者我认为是一个简单的案例实际上由于某种原因要复杂得多。
我主要想做的是

use std::env;
use std::convert::Infallible;
use std::net::SocketAddr;
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};

// A demo web server: takes a message on the command-line, then
// serves it back to incoming requests.

#[tokio::main]
pub async fn main() {
    let args: Vec<String> = env::args().collect();
    let message = format!("Arguments were: {:?}", &args[1..]);
    serve_message(message).await;
}

pub async fn serve_message(message: String) {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| {
        async move {
            Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
                Ok::<_, Infallible>(
                    Response::new(Body::from(message))
                )
            }))
        }
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

以下代码无法编译:

error[E0507]: cannot move out of `message`, a captured variable in an `FnMut` closure
  --> src/main.rs:22:68
   |
17 |   pub async fn serve_message(message: String) {
   |                              ------- captured outer variable
...
22 |               Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
   |  ____________________________________________-----------------------_^
   | |                                            |
   | |                                            captured by this `FnMut` closure
23 | |                 Ok::<_, Infallible>(
24 | |                     Response::new(Body::from(message))
   | |                                              -------
   | |                                              |
   | |                                              variable moved due to use in generator
   | |                                              move occurs because `message` has type `String`, which does not implement the `Copy` trait
25 | |                 )
26 | |             }))
   | |_____________^ move out of `message` occurs here

error[E0507]: cannot move out of `message`, a captured variable in an `FnMut` closure
  --> src/main.rs:21:9
   |
17 |   pub async fn serve_message(message: String) {
   |                              ------- captured outer variable
...
20 |       let make_svc = make_service_fn(|_conn| {
   |                                      ------- captured by this `FnMut` closure
21 | /         async move {
22 | |             Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
23 | |                 Ok::<_, Infallible>(
24 | |                     Response::new(Body::from(message))
   | |                                              -------
   | |                                              |
   | |                                              variable moved due to use in generator
   | |                                              move occurs because `message` has type `String`, which does not implement the `Copy` trait
25 | |                 )
26 | |             }))
27 | |         }
   | |_________^ move out of `message` occurs here

我已经尝试了各种各样的更复杂的修改,克隆,ARC,状态到一个带句柄impl的结构体中,以及许多其他方法,但我很挣扎,每一种方法似乎都让我回到了上面同样的基本问题。我显然错过了一些关于异步、闭包和所有权如何交互的重要内容,以及管理这些内容的工具。我见过类似的How to re-use a value from the outer scope inside a closure in Rust?,但唯一的答案是一个更简单的演示,并没有清楚地转化为更大的问题-只是添加.clone()的建议无处不在似乎不足以解决这个问题。
我觉得最令人困惑的部分是,这与Hyper自己的一个例子非常相似:但是https://docs.rs/hyper/latest/hyper/service/fn.make_service_fn.html#example那个例子似乎没有遇到任何问题,而这个例子却遇到了。
什么是正确的和惯用的方法来做到这一点,为什么它的工作,什么是这个和那个超级例子的情况下的区别?初学者水平的解释非常赞赏。

tf7tbtn2

tf7tbtn21#

出现此错误的原因是message字符串被移动到传递给service_fn的闭包中。在Rust中,每个值都有一个唯一的所有者,移动一个值会转移其所有权。一旦移动了值,就不能再从原始位置使用它。参见“Ownership and moves
但是,在您的例子中,您希望在多个响应中使用message字符串,这意味着您需要在多个闭包之间共享它。这就是Arc(原子引用计数)派上用场的地方。
Arc<T>是一个线程安全的引用计数指针,允许对T类型的值进行共享读取访问。它可以被克隆以创建指向相同值的新指针,从而增加引用计数。
您可以更新serve_message函数,将message Package 在Arc中,然后为每个请求like this (playground)克隆它:

use bytes::Bytes;
use hyper::{
    service::{make_service_fn, service_fn},
    Body, Error as HyperError, Request, Response, Server,
};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;

pub async fn serve_message(message: String) {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let message = Arc::new(Bytes::from(message));

    let make_svc = make_service_fn(move |_conn| {
        let message = Arc::clone(&message);
        async {
            Ok::<_, Infallible>(service_fn(move |_: Request<Body>| {
                let message = Bytes::copy_from_slice(&*Arc::clone(&message));
                async move { Ok::<_, HyperError>(Response::new(Body::from(message))) }
            }))
        }
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

它的工作方式是:

  • Arc::new(message)创建一个拥有message的新Arc。这是唯一直接拥有messageArc
  • 每次调用make_service_fn时,它都会克隆Arc(而不是message本身),这会增加引用计数,但不会移动message
  • service_fn中,我们再次为每个请求克隆Arc。这允许每个请求都引用message而不获取所有权。
  • 最后,Response::new(Body::from((*Arc::clone(&message)).clone()))再次克隆Arc,以便在响应中使用message。这不会移动message,并允许在后续响应中使用它。

关于最后一点:
代码Response::new(Body::from(Arc::clone(&message).to_string()))也可以正常工作,但需要记住一个关键的区别。
当您使用Arc::clone(&message).to_string()时,您正在为每个请求创建一个新的String。如果message很大或者有很多请求,这可能是低效的,因为每次都要为新的String分配内存。
let message = Bytes::copy_from_slice(&*Arc::clone(&message))的类型为Bytes
在这段代码中,我们使用Bytes::copy_from_slice(&*Arc::clone(&message))为每个请求从共享的Bytes创建一个新的Bytes示例。
这比直接共享Bytes效率低(我们希望这样做),但它避免了前面遇到的借用错误。
细目如下:

  • Arc::clone(&message)创建一个新的Arc,它指向与message相同的Bytes值。Arc::clone(&message)的类型是Arc<Bytes>
  • &*Arc::clone(&message)解引用Arc<Bytes>,以获得它所指向的Bytes值的引用。&*Arc::clone(&message)的类型是&Bytes
  • Bytes::copy_from_slice(&*Arc::clone(&message))创建一个新的Bytes值,该值包含与Bytesmessage指向的相同的字节序列。Bytes::copy_from_slice(&*Arc::clone(&message))的类型是Bytes

Bytes::copy_from_slice函数需要一个字节片(&[u8])的引用,而&Bytes可以用作&[u8],因为Bytes实现了Deref<Target=[u8]>
因此,Bytes::copy_from_slice(&*Arc::clone(&message))的总体效果是创建一个新的Bytes值,该值包含原始Bytes值的字节副本,允许它在响应体中独立使用。
解引用Arc<Bytes>以获得Bytes引用(&Bytes)不起作用,因为Body::from不接受&Bytes引用作为参数。
虽然Rust的From trait通常与引用一起工作,但在这种情况下,From trait仅针对拥有的Bytes类型实现,而不是针对Bytes的引用。

  • Body::from消耗其参数。它获取所提供值的所有权。
  • 当解引用Arc<Bytes>时,您将得到一个&Bytes(对Bytes的引用),而不是一个拥有的Bytes
  • &BytesBytes不是一回事。他们是不同的类型。前者是对Bytes值的引用,后者是 ownedBytes值。
  • 因为Body::from不是为&Bytes实现的,所以不能将&Bytes传递给Body::from

您所链接的Hyper示例创建了一个HTTP响应,其主体类型为hyper::Body::empty(),这并不涉及从外部作用域的任何借用。
在您的代码中,您试图使用服务函数外部作用域中的Stringmessage),这会导致您遇到的借用问题。
您的用例与Hyper示例不同,因为您希望在多个闭包之间共享拥有的String,这需要使用Arc,如上文所述。Arc允许多个闭包对同一个String有一个只读引用,而不需要获得它的所有权。

相关问题