Hyper中闭包和`async`块中的捕获量介绍

538 阅读9分钟

你好,Hyper!

对于那些不熟悉的人来说,Hyper是Rust的一个HTTP实现,建立在Tokio之上。它是一个低级别的库,为WarpRocket等框架以及reqwest客户端库提供动力。对于大多数人来说,大多数时候,使用像这样的高层次包装器是正确的事情。

但有时我们喜欢弄脏自己的手,有时直接用Hyper工作是正确的选择。而且从学习的角度来看,肯定是值得这样做的,至少是一次。还有什么比遵循Hyper主页上的例子更容易呢?要做到这一点,cargo new 一个新的项目,添加以下依赖项。

hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }

并将以下内容添加到main.rs

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

async fn hello_world(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new("Hello, World".into()))
}

#[tokio::main]
async fn main() {
    // We'll bind to 127.0.0.1:3000
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    // A `Service` is needed for every connection, so this
    // creates one from our `hello_world` function.
    let make_svc = make_service_fn(|_conn| async {
        // service_fn converts our function into a `Service`
        Ok::<_, Infallible>(service_fn(hello_world))
    });

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

    // Run this server for... forever!
    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

如果你有兴趣,在Hyper的网站上有一个关于这个代码的快速解释。但我们的重点将是对这段代码做一个永远微不足道的修改。开始吧!

计数器

还记得Geocities网站的好日子吗,每个页面都必须有一个访客计数器?我也想这样。让我们修改我们的hello_world 函数来做这件事。

use std::sync::{Arc, Mutex};

type Counter = Arc<Mutex<usize>>; // Bonus points: use an AtomicUsize instead

async fn hello_world(counter: Counter, _req: Request<Body>) -> Result<Response<Body>, Infallible> {
    let mut guard = counter.lock().unwrap(); // unwrap poisoned Mutexes
    *guard += 1;
    let message = format!("You are visitor number {}", guard);
    Ok(Response::new(message.into()))
}

这很容易,现在我们已经完成了hello_world 。唯一的问题是重写main ,向它传递一个Counter 的值。让我们在这个问题上做第一次天真的尝试。

let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let counter: Counter = Arc::new(Mutex::new(0));

let make_svc = make_service_fn(|_conn| async {
    Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
});

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

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

不幸的是,由于移出了捕获的变量,这个方法失败了。(这是我们在闭合训练模块中详细介绍的一个主题)。

error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
  --> src\main.rs:21:58
   |
18 |     let counter: Counter = Arc::new(Mutex::new(0));
   |         ------- captured outer variable
...
21 |         Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
   |                                                          ^^^^^^^ move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait

error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
  --> src\main.rs:20:50
   |
18 |       let counter: Counter = Arc::new(Mutex::new(0));
   |           ------- captured outer variable
19 |
20 |       let make_svc = make_service_fn(|_conn| async {
   |  __________________________________________________^
21 | |         Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
   | |                                        -------------------------------
   | |                                        |
   | |                                        move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
   | |                                        move occurs due to use in generator
22 | |     });
   | |_____^ move out of `counter` occurs here

克隆

这个错误并不太令人惊讶。我们把我们的Mutex 放在一个Arc 里面是有原因的:我们需要对它进行多次克隆,并把这些克隆传递给每个新的请求处理程序。但是我们还没有调用过一次clone!再一次,让我们做最天真的事情,改变一下。

Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))

Ok::<_, Infallible>(service_fn(|req| hello_world(counter.clone(), req)))

这就是错误信息开始变得更有趣的地方。

error[E0597]: `counter` does not live long enough
  --> src\main.rs:21:58
   |
20 |       let make_svc = make_service_fn(|_conn| async {
   |  ____________________________________-------_-
   | |                                    |
   | |                                    value captured here
21 | |         Ok::<_, Infallible>(service_fn(|req| hello_world(counter.clone(), req)))
   | |                                                          ^^^^^^^ borrowed value does not live long enough
22 | |     });
   | |_____- returning this value requires that `counter` is borrowed for `'static`
...
29 |   }
   |   - `counter` dropped here while still borrowed

async 块和闭包在默认情况下都会通过引用从环境中获取变量,而不是获取所有权。我们的闭包需要有一个'static 的生命周期,因此不能在我们的main 函数中保持对数据的引用。

move 万事俱备!

对此的标准解决方案是简单地将moves洒在每个async 块和闭包上。这将迫使每个闭包拥有Arc 本身,而不是对它的引用。这样做看起来很简单。

let make_svc = make_service_fn(move |_conn| async move {
    Ok::<_, Infallible>(service_fn(move |req| hello_world(counter.clone(), req)))
});

事实上,这确实解决了上面的错误。但它却给了我们一个新的错误。

error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
  --> src\main.rs:20:60
   |
18 |       let counter: Counter = Arc::new(Mutex::new(0));
   |           ------- captured outer variable
19 |
20 |       let make_svc = make_service_fn(move |_conn| async move {
   |  ____________________________________________________________^
21 | |         Ok::<_, Infallible>(service_fn(move |req| hello_world(counter.clone(), req)))
   | |                                        --------------------------------------------
   | |                                        |
   | |                                        move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
   | |                                        move occurs due to use in generator
22 | |     });
   | |_____^ move out of `counter` occurs here

双倍的闭包,双倍的克隆!

好吧,即使是这个错误也是很有意义的。让我们更好地理解我们的代码在做什么:

  • 创建一个闭包以传递给make_service_fn ,该闭包将为每一个新进入的连接被调用
  • 这个闭包中,创建一个新的闭包传递给service_fn ,这个闭包将为现有连接上的每一个新传入的请求被调用。

这就是直接使用Hyper工作的技巧所在。每一层闭包都需要拥有自己的Arc 的克隆。在我们上面的代码中,我们试图将Arc 从外层闭包的捕获变量转移到内层闭包的捕获变量。如果你仔细观察,这就是上面的错误信息所说的。我们的外层闭包是一个FnMut ,它必须可以被多次调用。因此,我们不能从它的捕获变量中移出。

看起来这应该是一个很容易解决的问题:只要再clone!

let make_svc = make_service_fn(move |_conn| async move {
    let counter_clone = counter.clone();
    Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
});

这就是我们遇到的真正的难题:我们得到了几乎完全相同的错误信息。

error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
  --> src\main.rs:20:60
   |
18 |       let counter: Counter = Arc::new(Mutex::new(0));
   |           ------- captured outer variable
19 |
20 |       let make_svc = make_service_fn(move |_conn| async move {
   |  ____________________________________________________________^
21 | |         let counter_clone = counter.clone();
   | |                             -------
   | |                             |
   | |                             move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
   | |                             move occurs due to use in generator
22 | |         Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
23 | |     });
   | |_____^ move out of `counter` occurs here

范式的转变

我们需要做的是稍微重写我们的代码,以揭示问题所在。让我们添加一堆不必要的大括号。我们将上面的代码。

let make_svc = make_service_fn(move |_conn| async move {
    let counter_clone = counter.clone();
    Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
});

变成这个语义相同的代码:

let make_svc = make_service_fn(move |_conn| { // outer closure
    async move { // async block
        let counter_clone = counter.clone();
        Ok::<_, Infallible>(service_fn(move |req| { // inner closure
            hello_world(counter_clone.clone(), req)
        }))
    }
});

错误信息基本上是相同的,只是来源位置略有不同。但现在我可以更正确地走完counter 的所有权。我在上面的代码中添加了注释,以强调三个不同的实体,它们可以通过某种环境获得值的所有权。

  • 外部闭包,它处理每个连接
  • 一个async 块,它构成了外层闭包的主体
  • 内部闭包,负责处理每个请求

在最初的代码结构中,我们把move |_conn| async move 挨在一起,至少对我来说,这掩盖了一个事实,即闭包和async 块是两个完全独立的实体。有了这个改变,让我们来跟踪counter 的所有权:

  1. 我们在main 函数中创建了Arc ;它被counter 变量所拥有。
  2. 我们将Arcmain 函数的counter 变量移到外层闭包的捕获变量中。
  3. 我们将counter 变量从外层闭包中移出,并进入async 块的捕获变量中。
  4. async 块的主体中,我们创建了一个counter 的克隆,称为counter_clone 。这并没有从async 块中移出,因为clone 方法只需要对Arc 的引用。
  5. 我们将Arccounter_clone 变量中移出,并进入内部闭包。
  6. 在内部闭包的主体中,我们克隆了Arc (正如(4)中所解释的,它不会移动),并将其传递到hello_world 函数中。

根据这个分解,你能看出问题出在哪里吗?是在步骤(3)。我们不想从外层闭包的捕获变量中移动出来。我们试图通过克隆counter 来避免这种移动。但我们克隆得太晚了!通过在async move 块内使用counter ,我们迫使编译器进行移动。万岁,我们已经发现了问题所在

非解决方法:不移动async

看起来我们在上面的 "洒上move"的尝试简直是好高骛远了。问题是,async 块正在夺取counter 的所有权。让我们尝试简单地删除那里的move 关键字。

let make_svc = make_service_fn(move |_conn| {
    async {
        let counter_clone = counter.clone();
        Ok::<_, Infallible>(service_fn(move |req| {
            hello_world(counter_clone.clone(), req)
        }))
    }
});

不幸的是,这并不是一个解决方案。

error: captured variable cannot escape `FnMut` closure body
  --> src\main.rs:21:9
   |
18 |       let counter: Counter = Arc::new(Mutex::new(0));
   |           ------- variable defined here
19 |
20 |       let make_svc = make_service_fn(move |_conn| {
   |                                                 - inferred to be a `FnMut` closure
21 | /         async {
22 | |             let counter_clone = counter.clone();
   | |                                 ------- variable captured here
23 | |             Ok::<_, Infallible>(service_fn(move |req| {
24 | |                 hello_world(counter_clone.clone(), req)
25 | |             }))
26 | |         }
   | |_________^ returns an `async` block that contains a reference to a captured variable, which then escapes the closure body
   |
   = note: `FnMut` closures only have access to their captured variables while they are executing...
   = note: ...therefore, they cannot allow references to captured variables to escape

这里的问题是,外层闭包将返回由async 块生成的Future 。而如果async 块没有movecounter ,它就会持有对外层闭包捕获变量的引用。而这是不允许的。

真正的解决方案:尽早克隆,经常克隆

好吧,撤销async moveasync 的转换,这是一个死胡同。事实证明,我们所要做的就是在启动async move 块之前克隆counter ,就像这样。

let make_svc = make_service_fn(move |_conn| {
    let counter_clone = counter.clone(); // this moved one line earlier
    async move {
        Ok::<_, Infallible>(service_fn(move |req| {
            hello_world(counter_clone.clone(), req)
        }))
    }
});

现在,我们在外层闭包中创建一个临时的counter_clone 。这是以引用的方式工作,因此不会移动任何东西。然后我们通过捕获将新的、临时的counter_clone 移到async move 块中,并从那里将其移到内部闭包中。这样,我们所有的闭包捕获的变量都没有被移动,因此,FnMut 的要求得到了满足。

就这样,我们终于可以享受Geocities访客计数器的光辉岁月了。

异步闭包

rustfmt 推荐的格式掩盖了这样一个事实,即在外部闭包和async block 之间有两个不同的环境在起作用,通过将两者移到move |_conn| async move 的一行中。这让人感觉这两个实体在某种程度上是一体的。但正如我们所展示的,它们并不是。

理论上这可以通过一个异步闭包来解决。nightly-2021-03-02我在#![feature(async_closure)] 上进行了测试,但没有找到使用异步闭包来解决这个问题的方法,与我上面的解决方法不同。但这可能是我自己对async_closure 的不熟悉。

目前,主要的收获是,闭包和async 块是两个不同的实体,各自有各自的环境。