Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


x-i18n: generated_at: “2026-03-01T14:48:35Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: c1539161fea534a89af45f1f47fad213f951a212bc7f5b41280a17394d8e0223 source_path: ch17-05-traits-for-async.md workflow: 16

深入了解异步相关的特征 (A Closer Look at the Traits for Async)

在本章中,我们以多种方式使用了 FutureStreamStreamExt 特征。然而,到目前为止,我们一直避免深入研究它们的工作原理或它们如何结合在一起的细节,这对于你日常的 Rust 工作来说通常是没问题的。但有时你会遇到一些需要更多地了解这些特征细节的情况,连同 Pin 类型和 Unpin 特征。在本节中,我们将进行足够的挖掘以在这些场景中提供帮助,而更深层次的研究仍留给其他文档。

Throughout the chapter, we’ve used the Future, Stream, and StreamExt traits in various ways. So far, though, we’ve avoided getting too far into the details of how they work or how they fit together, which is fine most of the time for your day-to-day Rust work. Sometimes, though, you’ll encounter situations where you’ll need to understand a few more of these traits’ details, along with the Pin type and the Unpin trait. In this section, we’ll dig in just enough to help in those scenarios, still leaving the really deep dive for other documentation.

Future 特征 (The Future Trait)

让我们首先仔细研究一下 Future 特征是如何工作的。以下是 Rust 对它的定义:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

该特征定义包含了一堆新类型,还有一些我们以前没见过的语法,所以让我们逐一分析这个定义。

That trait definition includes a bunch of new types and also some syntax we haven’t seen before, so let’s walk through the definition piece by piece.

首先, Future 的关联类型 Output 说明了 future 解析后的结果。这类似于 Iterator 特征的 Item 关联类型。其次, Future 具有 poll 方法,它为其 self 参数接收一个特殊的 Pin 引用,以及一个对 Context 类型的可变引用,并返回一个 Poll<Self::Output> 。我们稍后会更多地讨论 PinContext 。现在,让我们关注该方法的返回值,即 Poll 类型:

First, Future’s associated type Output says what the future resolves to. This is analogous to the Item associated type for the Iterator trait. Second, Future has the poll method, which takes a special Pin reference for its self parameter and a mutable reference to a Context type, and returns a Poll<Self::Output>. We’ll talk more about Pin and Context in a moment. For now, let’s focus on what the method returns, the Poll type:

#![allow(unused)]
fn main() {
pub enum Poll<T> {
    Ready(T),
    Pending,
}
}

这个 Poll 类型类似于 Option 。它有一个带有值的变体 Ready(T) 和一个没有值的变体 Pending 。不过, Poll 的含义与 Option 截然不同! Pending 变体表示 future 仍有工作要做,因此调用者稍后需要再次检查。 Ready 变体表示 Future 已经完成了它的工作,并且 T 值可用了。

This Poll type is similar to an Option. It has one variant that has a value, Ready(T), and one that does not, Pending. Poll means something quite different from Option, though! The Pending variant indicates that the future still has work to do, so the caller will need to check again later. The Ready variant indicates that the Future has finished its work and the T value is available.

注意:很少需要直接调用 poll ,但如果你确实需要,请记住,对于大多数 future,调用者在 future 返回 Ready 后不应再次调用 poll 。许多 future 如果在就绪后再被轮询,将会引发恐慌。可以安全再次轮询的 future 会在其文档中明确说明。这类似于 Iterator::next 的行为。

Note: It’s rare to need to call poll directly, but if you do need to, keep in mind that with most futures, the caller should not call poll again after the future has returned Ready. Many futures will panic if polled again after becoming ready. Futures that are safe to poll again will say so explicitly in their documentation. This is similar to how Iterator::next behaves.

当你看到使用 await 的代码时,Rust 会在底层将其编译为调用 poll 的代码。如果你回顾示例 17-4,我们在其中打印了单个 URL 解析后的页面标题,Rust 会将其编译成类似(尽管不完全是)这样的代码:

When you see code that uses await, Rust compiles it under the hood to code that calls poll. If you look back at Listing 17-4, where we printed out the page title for a single URL once it resolved, Rust compiles it into something kind of (although not exactly) like this:

match page_title(url).poll() {
    Ready(page_title) => match page_title {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
    Pending => {
        // 但这里应该放什么呢?
    }
}

当 future 仍处于 Pending 状态时,我们该怎么办?我们需要某种方法重试,再重试,一直重试,直到 future 最终就绪。换句话说,我们需要一个循环:

What should we do when the future is still Pending? We need some way to try again, and again, and again, until the future is finally ready. In other words, we need a loop:

let mut page_title_fut = page_title(url);
loop {
    match page_title_fut.poll() {
        Ready(value) => match page_title {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
        Pending => {
            // 继续循环
        }
    }
}

然而,如果 Rust 将其编译成完全那样的代码,那么每一个 await 都会是阻塞的——这恰恰与我们的初衷相反!相反,Rust 确保循环可以将控制权交给某个东西,而那个东西可以暂停该 future 的工作去处理其他 future,并在稍后再次检查这个 future。正如我们所见,那个东西就是一个异步运行时,而这种调度和协调工作是它的主要职责之一。

If Rust compiled it to exactly that code, though, every await would be blocking—exactly the opposite of what we were going for! Instead, Rust ensures that the loop can hand off control to something that can pause work on this future to work on other futures and then check this one again later. As we’ve seen, that something is an async runtime, and this scheduling and coordination work is one of its main jobs.

“使用消息传递在两个任务之间发送数据”一节中,我们描述了在 rx.recv 上的等待。 recv 调用返回一个 future,等待该 future 即是对其进行轮询。我们注意到,运行时将暂停该 future,直到它就绪,结果为 Some(message) 或通道关闭时的 None 。随着我们对 Future 特征,特别是 Future::poll 的深入理解,我们可以看到它是如何工作的。当 future 返回 Poll::Pending 时,运行时知道它还没就绪。反之,当 poll 返回 Poll::Ready(Some(message))Poll::Ready(None) 时,运行时知道 future “已”就绪并推进它。

In the “Sending Data Between Two Tasks Using Message Passing” section, we described waiting on rx.recv. The recv call returns a future, and awaiting the future polls it. We noted that a runtime will pause the future until it’s ready with either Some(message) or None when the channel closes. With our deeper understanding of the Future trait, and specifically Future::poll, we can see how that works. The runtime knows the future isn’t ready when it returns Poll::Pending. Conversely, the runtime knows the future is ready and advances it when poll returns Poll::Ready(Some(message)) or Poll::Ready(None).

关于运行时如何做到这一点的确切细节超出了本书的范围,但关键是要看到 future 的基本机制:运行时对其负责的每个 future 进行“轮询 (polls)”,在 future 尚未就绪时将其放回休眠状态。

The exact details of how a runtime does that are beyond the scope of this book, but the key is to see the basic mechanics of futures: a runtime polls each future it is responsible for, putting the future back to sleep when it is not yet ready.

Pin 类型与 Unpin 特征 (The Pin Type and the Unpin Trait)

回到示例 17-13,我们使用 trpl::join! 宏来等待三个 future。然而,拥有一个包含一定数量 future(直到运行时才确定)的集合(如向量)是很常见的。让我们将示例 17-13 更改为示例 17-23 中的代码,将三个 future 放入一个向量并改为调用 trpl::join_all 函数,这段代码目前还无法编译。

Back in Listing 17-13, we used the trpl::join! macro to await three futures. However, it’s common to have a collection such as a vector containing some number futures that won’t be known until runtime. Let’s change Listing 17-13 to the code in Listing 17-23 that puts the three futures into a vector and calls the trpl::join_all function instead, which won’t compile yet.

{{#rustdoc_include ../listings/ch17-async-await/listing-17-23/src/main.rs:here}}

我们将每个 future 放在一个 Box 中,使它们成为“特征对象 (trait objects)”,就像我们在第 12 章的“从 run 返回错误”一节中所做的那样。(我们将在第 18 章详细介绍特征对象。)使用特征对象让我们能够将这些类型产生的每个匿名 future 视为相同的类型,因为它们都实现了 Future 特征。

We put each future within a Box to make them into trait objects, just as we did in the “Returning Errors from run” section in Chapter 12. (We’ll cover trait objects in detail in Chapter 18.) Using trait objects lets us treat each of the anonymous futures produced by these types as the same type, because all of them implement the Future trait.

这可能令人惊讶。毕竟,这些异步块都没有返回任何内容,所以每个异步块都产生一个 Future<Output = ()> 。请记住, Future 是一个特征,并且编译器为每个异步块创建一个唯一的枚举,即使它们具有相同的输出类型。就像你不能在 Vec 中放入两个不同的手写结构体一样,你也不能混合编译器生成的枚举。

This might be surprising. After all, none of the async blocks returns anything, so each one produces a Future<Output = ()>. Remember that Future is a trait, though, and that the compiler creates a unique enum for each async block, even when they have identical output types. Just as you can’t put two different handwritten structs in a Vec, you can’t mix compiler-generated enums.

然后我们将 future 集合传递给 trpl::join_all 函数并 await 结果。然而,这段代码无法编译;以下是错误消息的相关部分。

Then we pass the collection of futures to the trpl::join_all function and await the result. However, this doesn’t compile; here’s the relevant part of the error messages.

error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
  --> src/main.rs:48:33
   |
48 |         trpl::join_all(futures).await;
   |                                 ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
   |
   = note: consider using the `pin!` macro
           consider using `Box::pin` if you need to access the pinned value outside of the current scope
   = note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
  --> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
   |
27 | pub struct JoinAll<F>
   |            ------- required by a bound in this struct
28 | where
29 |     F: Future,
   |        ^^^^^^ required by this bound in `JoinAll`

此错误消息中的提示告诉我们,我们应该使用 pin! 宏来“固定 (pin)”这些值,这意味着将它们放入 Pin 类型中,以保证这些值在内存中不会被移动。错误消息显示需要固定是因为 dyn Future<Output = ()> 需要实现 Unpin 特征,而它目前还没有实现。

The note in this error message tells us that we should use the pin! macro to pin the values, which means putting them inside the Pin type that guarantees the values won’t be moved in memory. The error message says pinning is required because dyn Future<Output = ()> needs to implement the Unpin trait and it currently does not.

trpl::join_all 函数返回一个名为 JoinAll 的结构体。该结构体对类型 F 是泛型的,且 F 被约束为实现 Future 特征。直接使用 await 等待一个 future 会隐式地固定该 future。这就是为什么我们不需要在想要 await future 的每个地方都使用 pin!

The trpl::join_all function returns a struct called JoinAll. That struct is generic over a type F, which is constrained to implement the Future trait. Directly awaiting a future with await pins the future implicitly. That’s why we don’t need to use pin! everywhere we want to await futures.

然而,我们这里并不是直接等待一个 future。相反,我们通过将 future 集合传递给 join_all 函数来构造一个新的 future,即 JoinAll。 join_all 的签名要求集合中各项的类型都实现 Future 特征,而 Box<T> 仅在它包裹的 T 是实现了 Unpin 特征的 future 时才实现 Future

However, we’re not directly awaiting a future here. Instead, we construct a new future, JoinAll, by passing a collection of futures to the join_all function. The signature for join_all requires that the types of the items in the collection all implement the Future trait, and Box<T> implements Future only if the T it wraps is a future that implements the Unpin trait.

需要消化的东西真多!为了真正理解它,让我们进一步深入了解 Future 特征究竟是如何工作的,特别是围绕“固定 (pinning)”。再次查看 Future 特征的定义:

That’s a lot to absorb! To really understand it, let’s dive a little further into how the Future trait actually works, in particular around pinning. Look again at the definition of the Future trait:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    // 所需方法
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

cx 参数及其 Context 类型是运行时在保持惰性的同时真正知道何时检查任何给定 future 的关键。同样,该过程如何工作的细节超出了本章的范围,通常只有在编写自定义 Future 实现时才需要考虑这一点。我们将重点放在 self 的类型上,因为这是我们第一次见到 self 带有类型标注的方法。对 self 的类型标注与对其他函数参数的类型标注类似,但有两个关键区别:

The cx parameter and its Context type are the key to how a runtime actually knows when to check any given future while still being lazy. Again, the details of how that works are beyond the scope of this chapter, and you generally only need to think about this when writing a custom Future implementation. We’ll focus instead on the type for self, as this is the first time we’ve seen a method where self has a type annotation. A type annotation for self works like type annotations for other function parameters but with two key differences:

  • 它告诉 Rust 调用该方法时 self 必须是什么类型。

  • 它不能是随便任何类型。它仅限于实现该方法的类型、该类型的引用或智能指针,或者是包裹了该类型引用的 Pin

  • It tells Rust what type self must be for the method to be called.

  • It can’t be just any type. It’s restricted to the type on which the method is implemented, a reference or smart pointer to that type, or a Pin wrapping a reference to that type.

我们将在第 18 章中看到更多关于此语法的内容。目前,只需知道如果我们想轮询一个 future 来检查它是 Pending 还是 Ready(Output) ,我们需要一个包裹了该类型的可变引用的 Pin

We’ll see more on this syntax in Chapter 18. For now, it’s enough to know that if we want to poll a future to check whether it is Pending or Ready(Output), we need a Pin-wrapped mutable reference to the type.

Pin 是对指针类类型(如 &&mutBoxRc )的包装。(从技术上讲, Pin 适用于实现了 DerefDerefMut 特征的类型,但这实际上等同于仅处理引用和智能指针。) Pin 本身不是指针,并且不像 RcArc 那样具有引用计数的行为;它纯粹是编译器用来强制执行指针使用约束的工具。

Pin is a wrapper for pointer-like types such as &, &mut, Box, and Rc. (Technically, Pin works with types that implement the Deref or DerefMut traits, but this is effectively equivalent to working only with references and smart pointers.) Pin is not a pointer itself and doesn’t have any behavior of its own like Rc and Arc do with reference counting; it’s purely a tool the compiler can use to enforce constraints on pointer usage.

回想一下 await 是根据对 poll 的调用实现的,这开始解释了我们之前看到的错误消息,但那是关于 Unpin 的,而不是 Pin 。那么 Pin 究竟与 Unpin 有什么关系,为什么 Future 需要 self 处于 Pin 类型中才能调用 poll 呢?

Recalling that await is implemented in terms of calls to poll starts to explain the error message we saw earlier, but that was in terms of Unpin, not Pin. So how exactly does Pin relate to Unpin, and why does Future need self to be in a Pin type to call poll?

请记住本章前面的内容:future 中一系列的等待点会被编译成一个状态机,并且编译器会确保该状态机遵循 Rust 关于安全性的所有常规规则,包括借用和所有权。为了做到这一点,Rust 会查看一个等待点与下一个等待点或异步块结束之间需要哪些数据。然后它在编译后的状态机中创建一个相应的变体。每个变体都获得了它所需的访问该源代码部分将使用的数据的权限,无论是通过获取该数据的所有权还是获取其可变或不可变引用。

Remember from earlier in this chapter that a series of await points in a future get compiled into a state machine, and the compiler makes sure that state machine follows all of Rust’s normal rules around safety, including borrowing and ownership. To make that work, Rust looks at what data is needed between one await point and either the next await point or the end of the async block. It then creates a corresponding variant in the compiled state machine. Each variant gets the access it needs to the data that will be used in that section of the source code, whether by taking ownership of that data or by getting a mutable or immutable reference to it.

到目前为止一切顺利:如果我们弄错了给定异步块中的所有权或引用,借用检查器会告诉我们。当我们想要移动与该块相对应的 future 时——比如将其移动到一个 Vec 中传递给 join_all ——事情就变得棘手了。

So far, so good: if we get anything wrong about the ownership or references in a given async block, the borrow checker will tell us. When we want to move around the future that corresponds to that block—like moving it into a Vec to pass to join_all—things get trickier.

当我们移动一个 future 时——无论是通过将其推入数据结构中作为迭代器与 join_all 一起使用,还是通过从函数返回它——这实际上意味着移动 Rust 为我们创建的状态机。与 Rust 中的大多数其他类型不同,Rust 为异步块创建的 future 最终可能会在其任何给定变体的字段中包含对自身的引用,如图 17-4 的简化插图所示。

When we move a future—whether by pushing it into a data structure to use as an iterator with join_all or by returning it from a function—that actually means moving the state machine Rust creates for us. And unlike most other types in Rust, the futures Rust creates for async blocks can end up with references to themselves in the fields of any given variant, as shown in the simplified illustration in Figure 17-4.

一个单列三行的表格代表一个 future fut1,它在前两行中具有数据值 0 和 1,并且有一个从第三行指向第二行的箭头,代表 future 内部的一个引用。
图 17-4:一个自引用数据类型

然而,默认情况下,任何具有自引用的对象移动起来都是不安全的,因为引用始终指向它们所引用内容的实际内存地址(见图 17-5)。如果你移动数据结构本身,那些内部引用将仍指向旧位置。然而,那个内存位置现在是无效的。一方面,当你对数据结构进行更改时,它的值将不会更新。另一方面——更重要的一点是——计算机现在可以自由地将该内存用于其他目的!你稍后可能会读到完全不相关的数据。

By default, though, any object that has a reference to itself is unsafe to move, because references always point to the actual memory address of whatever they refer to (see Figure 17-5). If you move the data structure itself, those internal references will be left pointing to the old location. However, that memory location is now invalid. For one thing, its value will not be updated when you make changes to the data structure. For another—more important—thing, the computer is now free to reuse that memory for other purposes! You could end up reading completely unrelated data later.

两个表格描绘了两个 future fut1 和 fut2,每个表格都有一列和三行,代表将一个 future 从 fut1 移动到 fut2 的结果。第一个 fut1 变灰了,每个索引中都有一个问号,代表未知内存。第二个 fut2 在第一行和第二行中有 0 和 1,并且有一个从它的第三行指回 fut1 第二行的箭头,代表一个指向 future 移动前在内存中旧位置的指针。
图 17-5:移动自引用数据类型的不安全结果

从理论上讲,Rust 编译器可以尝试在对象每次被移动时更新它的每一个引用,但这可能会增加大量的性能开销,特别是如果需要更新整个引用网络。如果我们能确保所讨论的数据结构“在内存中不移动”,我们就不用更新任何引用了。这正是 Rust 借用检查器的用途:在安全代码中,它会阻止你移动任何带有一个活动引用的项。

Theoretically, the Rust compiler could try to update every reference to an object whenever it gets moved, but that could add a lot of performance overhead, especially if a whole web of references needs updating. If we could instead make sure the data structure in question doesn’t move in memory, we wouldn’t have to update any references. This is exactly what Rust’s borrow checker is for: in safe code, it prevents you from moving any item with an active reference to it.

Pin 在此基础上为我们提供了我们需要的精确保证。当我们通过将指向某个值的指针包装在 Pin 中来“固定 (pin)”该值时,它就不再能移动。因此,如果你拥有 Pin<Box<SomeType>> ,你实际上固定的是 SomeType 值,而“不是” Box 指针。图 17-6 说明了这个过程。

Pin builds on that to give us the exact guarantee we need. When we pin a value by wrapping a pointer to that value in Pin, it can no longer move. Thus, if you have Pin<Box<SomeType>>, you actually pin the SomeType value, not the Box pointer. Figure 17-6 illustrates this process.

三个方框并排摆放。第一个标记为“Pin”,第二个标记为“b1”,第三个标记为“pinned”。在“pinned”中是一个标记为“fut”的表格,只有一列;它代表一个 future,其中有数据结构各部分的单元格。它的第一个单元格值为“0”,第二个单元格有一个从中出来的箭头,指向第四个也是最后一个单元格,该单元格中值为“1”,第三个单元格有虚线和省略号,表示数据结构可能还有其他部分。总的来说,“fut”表格代表一个自引用的 future。一个箭头从标记为“Pin”的方框出发,穿过标记为“b1”的方框,并在“pinned”方框内的“fut”表格处终止。
图 17-6:固定一个指向自引用 future 类型的 `Box`

实际上, Box 指针仍然可以自由移动。记住:我们关心的是确保最终被引用的数据保持在原位。如果指针移动了,但它指向的数据仍在同一个地方,如图 17-7 所示,那么就没有潜在问题。(作为一项独立练习,查看这些类型的文档以及 std::pin 模块,并尝试弄清楚你将如何使用包裹 BoxPin 来做到这一点。)关键是自引用类型本身不能移动,因为它仍然是被固定的。

In fact, the Box pointer can still move around freely. Remember: we care about making sure the data ultimately being referenced stays in place. If a pointer moves around, but the data it points to is in the same place, as in Figure 17-7, there’s no potential problem. (As an independent exercise, look at the docs for the types as well as the std::pin module and try to work out how you’d do this with a Pin wrapping a Box.) The key is that the self-referential type itself cannot move, because it is still pinned.

四个方框摆放在大致三列中,与前一个图表相同,只是第二列发生了变化。现在第二列中有两个方框,分别标记为“b1”和“b2”,“b1”变灰了,从“Pin”发出的箭头穿过“b2”而不是“b1”,表明指针已经从“b1”移动到了“b2”,但“pinned”中的数据没有移动。
图 17-7:移动指向自引用 future 类型的 `Box`

然而,大多数类型在移动时是完全安全的,即使它们恰好位于 Pin 指针之后。我们只需要在项具有内部引用时考虑固定。原始值(如数字和布尔值)是安全的,因为它们显然没有任何内部引用。你在 Rust 中通常使用的大多数类型也一样。例如,你可以放心地移动 Vec 。鉴于我们目前所见,如果你有一个 Pin<Vec<String>> ,你将不得不通过 Pin 提供的安全但有限制的 API 来执行所有操作,尽管在没有其他引用的情况下, Vec<String> 移动起来总是安全的。我们需要一种方法告诉编译器,在类似这样的情况下移动项是可以的——这就是 Unpin 发挥作用的地方。

However, most types are perfectly safe to move around, even if they happen to be behind a Pin pointer. We only need to think about pinning when items have internal references. Primitive values such as numbers and Booleans are safe because they obviously don’t have any internal references. Neither do most types you normally work with in Rust. You can move around a Vec, for example, without worrying. Given what we have seen so far, if you have a Pin<Vec<String>>, you’d have to do everything via the safe but restrictive APIs provided by Pin, even though a Vec<String> is always safe to move if there are no other references to it. We need a way to tell the compiler that it’s fine to move items around in cases like this—and that’s where Unpin comes into play.

Unpin 是一个标记特征,类似于我们在第 16 章中看到的 SendSync 特征,因此它本身没有功能。标记特征的存在只是为了告诉编译器在特定上下文中使用实现该特征的类型是安全的。 Unpin 通知编译器,给定类型“不需要”遵守任何关于所讨论的值是否可以安全移动的保证。

Unpin is a marker trait, similar to the Send and Sync traits we saw in Chapter 16, and thus has no functionality of its own. Marker traits exist only to tell the compiler it’s safe to use the type implementing a given trait in a particular context. Unpin informs the compiler that a given type does not need to uphold any guarantees about whether the value in question can be safely moved.

就像 SendSync 一样,对于所有编译器可以证明其安全的类型,编译器都会自动实现 Unpin 。一个特殊情况(同样类似于 SendSync )是“未”为某种类型实现 Unpin 的情况。其记法是 impl !Unpin for SomeType ,其中 SomeType 是一个在 Pin 中使用该类型的指针时“确实”需要遵守这些保证才能保持安全的类型名称。

Just as with Send and Sync, the compiler implements Unpin automatically for all types where it can prove it is safe. A special case, again similar to Send and Sync, is where Unpin is not implemented for a type. The notation for this is impl !Unpin for SomeType, where SomeType is the name of a type that does need to uphold those guarantees to be safe whenever a pointer to that type is used in a Pin.

换句话说,关于 PinUnpin 之间的关系,有两点需要记住。首先, Unpin 是“正常”情况,而 !Unpin 是特殊情况。其次,一个类型实现的是 Unpin 还是 !Unpin ,只有当你使用指向该类型的固定指针(如 Pin<&mut SomeType> )时才有意义。

In other words, there are two things to keep in mind about the relationship between Pin and Unpin. First, Unpin is the “normal” case, and !Unpin is the special case. Second, whether a type implements Unpin or !Unpin only matters when you’re using a pinned pointer to that type like Pin<&mut SomeType>.

具体来说,想一想 String :它具有长度和组成它的 Unicode 字符。我们可以将 String 包装在 Pin 中,如图 17-8 所示。然而, String 会自动实现 Unpin ,正如 Rust 中的大多数其他类型一样。

To make that concrete, think about a String: it has a length and the Unicode characters that make it up. We can wrap a String in Pin, as seen in Figure 17-8. However, String automatically implements Unpin, as do most other types in Rust.

左边是一个标记为“Pin”的方框,有一个箭头从它指向右边一个标记为“String”的方框。“String”方框包含数据 5usize(代表字符串长度),以及字母“h”、“e”、“l”、“l”和“o”(代表该 String 实例中存储的字符串“hello”的字符)。虚线矩形包围了“String”方框及其标签,但没有包围“Pin”方框。
图 17-8:固定一个 `String` ;虚线表示 `String` 实现了 `Unpin` 特征,因此它没有被固定

结果是,我们可以做一些如果 String 实现的是 !Unpin 则会非法的事情,例如在内存中的完全相同位置将一个字符串替换为另一个字符串,如图 17-9 所示。这并没有违反 Pin 合同,因为 String 没有使其在移动时变得不安全的内部引用。这正是为什么它实现的是 Unpin 而不是 !Unpin 的原因。

As a result, we can do things that would be illegal if String implemented !Unpin instead, such as replacing one string with another at the exact same location in memory as in Figure 17-9. This doesn’t violate the Pin contract, because String has no internal references that make it unsafe to move around. That is precisely why it implements Unpin rather than !Unpin.

来自前一个例子的相同“hello”字符串数据,现在标记为“s1”并变灰了。前一个例子中的“Pin”方框现在指向一个不同的 String 实例,该实例标记为“s2”,它是有效的,长度为 7usize,包含字符串“goodbye”的字符。s2 被虚线矩形包围,因为它也实现了 Unpin 特征。
图 17-9:在内存中用一个完全不同的 `String` 替换该 `String`

现在我们有足够的知识来理解示例 17-23 中那个 join_all 调用所报告的错误了。我们最初尝试将由异步块产生的 future 移动到 Vec<Box<dyn Future<Output = ()>>> 中,但正如我们所见,那些 future 可能具有内部引用,因此它们不会自动实现 Unpin 。一旦我们将它们固定,我们就可以将生成的 Pin 类型传递到 Vec 中,确信 future 中的底层数据将“不”被移动。示例 17-24 展示了如何通过在定义三个 future 的地方调用 pin! 宏并调整特征对象类型来修复代码。

Now we know enough to understand the errors reported for that join_all call from back in Listing 17-23. We originally tried to move the futures produced by async blocks into a Vec<Box<dyn Future<Output = ()>>>, but as we’ve seen, those futures may have internal references, so they don’t automatically implement Unpin. Once we pin them, we can pass the resulting Pin type into the Vec, confident that the underlying data in the futures will not be moved. Listing 17-24 shows how to fix the code by calling the pin! macro where each of the three futures are defined and adjusting the trait object type.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-24/src/main.rs:here}}
}

这个例子现在可以编译并运行了,我们可以动态地向向量添加或从中移除 future 并将它们全部联接。

This example now compiles and runs, and we could add or remove futures from the vector at runtime and join them all.

相对于日常的 Rust 代码, PinUnpin 主要在构建底层库或构建运行时本身时才显得重要。不过,现在当你在错误消息中看到这些特征时,你应该对如何修复代码有了更好的想法!

Pin and Unpin are mostly important for building lower-level libraries, or when you’re building a runtime itself, rather than for day-to-day Rust code. When you see these traits in error messages, though, now you’ll have a better idea of how to fix your code!

注意: PinUnpin 的这种组合使得在 Rust 中安全地实现一整类复杂的类型成为可能,否则这些类型由于自引用而会极具挑战。需要 Pin 的类型在当今的异步 Rust 中最常出现,但偶尔你也会在其他上下文中看到它们。

关于 PinUnpin 如何工作以及它们被要求遵守的规则的细节,在 std::pin 的 API 文档中得到了广泛的涵盖,所以如果你有兴趣了解更多,那是一个绝佳的起点。

如果你想更详细地了解底层工作原理,请参阅《Rust 异步编程》中的第 2 章第 4 章

Note: This combination of Pin and Unpin makes it possible to safely implement a whole class of complex types in Rust that would otherwise prove challenging because they’re self-referential. Types that require Pin show up most commonly in async Rust today, but every once in a while, you might see them in other contexts, too.

The specifics of how Pin and Unpin work, and the rules they’re required to uphold, are covered extensively in the API documentation for std::pin, so if you’re interested in learning more, that’s a great place to start.

If you want to understand how things work under the hood in even more detail, see Chapters 2 and 4 of Asynchronous Programming in Rust.

Stream 特征 (The Stream Trait)

The Stream Trait

现在你已经对 FuturePinUnpin 特征有了更深入的了解,我们可以将注意力转向 Stream 特征了。正如你在本章前面学到的,流类似于异步迭代器。然而,与 IteratorFuture 不同的是,截至撰写本文时, Stream 在标准库中还没有定义,但在整个生态系统中都有使用来自 futures crate 的一个非常通用的定义。

Now that you have a deeper grasp on the Future, Pin, and Unpin traits, we can turn our attention to the Stream trait. As you learned earlier in the chapter, streams are similar to asynchronous iterators. Unlike Iterator and Future, however, Stream has no definition in the standard library as of this writing, but there is a very common definition from the futures crate used throughout the ecosystem.

在研究 Stream 特征可能如何将它们融合在一起之前,让我们先回顾一下 IteratorFuture 特征的定义。从 Iterator 来看,我们有了序列的概念:它的 next 方法提供一个 Option<Self::Item> 。从 Future 来看,我们有了随时间就绪的概念:它的 poll 方法提供一个 Poll<Self::Output> 。为了代表随时间变得就绪的一系列项,我们定义了一个将这些特性结合在一起的 Stream 特征:

Let’s review the definitions of the Iterator and Future traits before looking at how a Stream trait might merge them together. From Iterator, we have the idea of a sequence: its next method provides an Option<Self::Item>. From Future, we have the idea of readiness over time: its poll method provides a Poll<Self::Output>. To represent a sequence of items that become ready over time, we define a Stream trait that puts those features together:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
}

Stream 特征为由该流产生的项类型定义了一个名为 Item 的关联类型。这类似于 Iterator ,可能有零到多个项,而不像 Future ,总是有单个 Output ,即使它是单元类型 ()

The Stream trait defines an associated type called Item for the type of the items produced by the stream. This is similar to Iterator, where there may be zero to many items, and unlike Future, where there is always a single Output, even if it’s the unit type ().

Stream 还定义了一个获取这些项的方法。我们称之为 poll_next ,以明确它以与 Future::poll 相同的方式进行轮询,并以与 Iterator::next 相同的方式产生一系列项。它的返回类型结合了 PollOption 。外部类型是 Poll ,因为它必须像 future 一样检查就绪状态。内部类型是 Option ,因为它需要像迭代器一样发出是否还有更多消息的信号。

Stream also defines a method to get those items. We call it poll_next, to make it clear that it polls in the same way Future::poll does and produces a sequence of items in the same way Iterator::next does. Its return type combines Poll with Option. The outer type is Poll, because it has to be checked for readiness, just as a future does. The inner type is Option, because it needs to signal whether there are more messages, just as an iterator does.

与此定义非常相似的东西很可能会最终成为 Rust 标准库的一部分。与此同时,它是大多数运行时工具包的一部分,因此你可以依赖它,接下来我们介绍的一切通常都适用!

Something very similar to this definition will likely end up as part of Rust’s standard library. In the meantime, it’s part of the toolkit of most runtimes, so you can rely on it, and everything we cover next should generally apply!

不过,在我们在“流:顺序排列的 Future”一节中看到的例子中,我们没有使用 poll_nextStream ,而是使用了 nextStreamExt 。当然,我们可以通过手写我们自己的 Stream 状态机直接根据 poll_next API 工作,就像我们可以通过 future 的 poll 方法直接与它们交互一样。然而,使用 await 会美观得多,而 StreamExt 特征提供了 next 方法,这样我们就可以做到这一点:

In the examples we saw in the “Streams: Futures in Sequence” section, though, we didn’t use poll_next or Stream, but instead used next and StreamExt. We could work directly in terms of the poll_next API by hand-writing our own Stream state machines, of course, just as we could work with futures directly via their poll method. Using await is much nicer, though, and the StreamExt trait supplies the next method so we can do just that:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/no-listing-stream-ext/src/lib.rs:here}}
}

注意:我们在本章前面使用的实际定义看起来与此略有不同,因为它支持尚不支持在特征中使用异步函数的 Rust 版本。因此,它看起来像这样:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

那个 Next 类型是一个实现了 Futurestruct ,并允许我们通过 Next<'_, Self> 为对 self 的引用命名生命周期,以便 await 可以与该方法配合工作。

Note: The actual definition we used earlier in the chapter looks slightly different than this, because it supports versions of Rust that did not yet support using async functions in traits. As a result, it looks like this:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

That Next type is a struct that implements Future and allows us to name the lifetime of the reference to self with Next<'_, Self>, so that await can work with this method.

StreamExt 特征也是所有可供流使用的有趣方法的归宿。 StreamExt 会为每个实现 Stream 的类型自动实现,但这些特征是分开定义的,以便社区能够迭代便利 API 而不影响基础特征。

The StreamExt trait is also the home of all the interesting methods available to use with streams. StreamExt is automatically implemented for every type that implements Stream, but these traits are defined separately to enable the community to iterate on convenience APIs without affecting the foundational trait.

trpl crate 中使用的 StreamExt 版本中,该特征不仅定义了 next 方法,还提供了一个正确处理调用 Stream::poll_next 细节的 next 默认实现。这意味着即使当你需要编写自己的流数据类型时,你“仅”需要实现 Stream ,然后任何使用你数据类型的人都可以自动使用 StreamExt 及其方法。

In the version of StreamExt used in the trpl crate, the trait not only defines the next method but also supplies a default implementation of next that correctly handles the details of calling Stream::poll_next. This means that even when you need to write your own streaming data type, you only have to implement Stream, and then anyone who uses your data type can use StreamExt and its methods with it automatically.

关于这些特征底层细节的内容我们就讲到这里。为了总结一下,让我们考虑一下 future(包括流)、任务和线程是如何配合在一起的!

That’s all we’re going to cover for the lower-level details on these traits. To wrap up, let’s consider how futures (including streams), tasks, and threads all fit together!