深入探索异步相关的 Trait
A Closer Look at the Traits for Async
在本章中,我们以各种方式使用了 Future、Stream 和 StreamExt trait。然而,到目前为止,我们一直避免深入研究它们的工作原理或它们是如何结合在一起的细节,这对于你日常的 Rust 工作来说通常没问题。但有时,你会遇到需要理解这些 trait 的更多细节,以及 Pin 类型和 Unpin trait 的情况。在本节中,我们将深入研究到足以在这些场景中提供帮助的程度,而将 真正的 深度研究留给其他文档。
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 Trait
The Future Trait
让我们首先仔细看看 Future trait 是如何工作的。以下是 Rust 对它的定义:
Let’s start by taking a closer look at how the Future trait works. Here’s how
Rust defines it:
#![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>;
}
}
该 trait 定义包含许多新类型,还有一些我们以前没见过的语法,所以让我们逐一分析这个定义。
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 trait 的 Item 关联类型。其次,Future 拥有 poll 方法,它的 self 参数采用特殊的 Pin 引用,并接受一个指向 Context 类型的可变引用,然后返回一个 Poll<Self::Output>。我们稍后会详细讨论 Pin 和 Context。现在,让我们专注于该方法返回的内容,即 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 在就绪后如果再次被轮询,将会触发 panic。可以安全再次轮询的 future 会在其文档中明确说明。这类似于Iterator::next的行为。
Note: It’s rare to need to call
polldirectly, but if you do need to, keep in mind that with most futures, the caller should not callpollagain after the future has returnedReady. 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 howIterator::nextbehaves.
当你看到使用 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 => {
// 但这里该写什么呢?
// But what goes here?
}
}
当 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 => {
// 继续循环
// continue
}
}
}
然而,如果 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 trait,特别是 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,当 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 Trait
The Pin Type and the Unpin Trait
回到示例 17-13,我们使用了 trpl::join! 宏来等待三个 future。然而,通常会有像 vector 这样的集合包含一些直到运行时才知道数量的 future。让我们将示例 17-13 更改为示例 17-23 中的代码,将三个 future 放入一个 vector 并改为调用 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.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
我们将每个 future 放入 Box 中,使它们成为 trait 对象,就像我们在第 12 章的“从 run 返回错误”一节中所做的那样。(我们将在第 18 章详细介绍 trait 对象。)使用 trait 对象允许我们将这些类型产生的每个匿名 future 视为相同的类型,因为它们都实现了 Future trait。
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 是一个 trait,编译器会为每个异步块创建一个唯一的枚举,即使它们的输出类型相同。就像你不能在 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 函数并等待结果。然而,这无法编译;以下是错误消息的相关部分。
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 trait,而它目前没有实现。
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 trait。直接使用 await 等待一个 future 会隐式地固定该 future。这就是为什么我们不需要在每个想要等待 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。相反,我们通过向 join_all 函数传递一个 future 集合来构造一个新的 future:JoinAll。join_all 的签名要求集合中项的类型都实现 Future trait,而 Box<T> 只有在它包装的 T 是实现了 Unpin trait 的 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 trait 的实际工作原理,特别是关于固定的部分。再次看看 Future trait 的定义:
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;
// 所需方法
// Required method
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
selfmust 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
Pinwrapping 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 是指针类类型(如 &、&mut、Box 和 Rc)的包装器。(从技术上讲,Pin 适用于实现了 Deref 或 DerefMut trait 的类型,但这实际上等同于仅适用于引用和智能指针。)Pin 本身不是指针,也不像 Rc 和 Arc 那样通过引用计数拥有自己的行为;它纯粹是编译器用来对指针使用强制执行约束的工具。
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.
然而,默认情况下,任何包含对自身引用的对象在移动时都是不安全的,因为引用总是指向它们所引用的任何东西的实际内存地址(见图 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.
理论上,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.
事实上,Box 指针仍然可以自由移动。请记住:我们关心的是确保最终被引用的数据保持在原位。如果指针移动了,但它指向的数据 仍在原位,如图 17-7 所示,就不会有潜在的问题。(作为一项独立练习,请查看类型文档以及 std::pin 模块,并尝试弄清楚如何使用包装了 Box 的 Pin 来做到这一点。)关键是自引用类型本身不能移动,因为它仍处于固定状态。
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.
然而,大多数类型即使位于 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, you’d have to do everything via the safe but restrictive APIs provided byPin, even though a Vecis 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 whereUnpin` comes into play.
Unpin 是一个标记 trait,类似于我们在第 16 章看到的 Send 和 Sync trait,因此它本身没有功能。标记 trait 的存在只是为了告诉编译器,在特定上下文中使用实现给定 trait 的类型是安全的。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.
就像 Send 和 Sync 一样,编译器会自动为所有它能证明安全的类型实现 Unpin。同样类似于 Send 和 Sync 的特殊情况是,没有为某个类型实现 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.
换句话说,关于 Pin 和 Unpin 之间的关系,有两点需要记住。首先,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.
因此,我们可以执行一些如果 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.
现在我们有了足够的知识来理解示例 17-23 中 join_all 调用报告的错误。我们最初尝试将异步块产生的 future 移动到 Vec<Box<dyn Future<Output = ()>>> 中,但正如我们所见,这些 future 可能包含内部引用,因此它们不会自动实现 Unpin。一旦我们固定了它们,我们就可以将生成的 Pin 类型传入 Vec,并确信 future 中的底层数据 不会 被移动。示例 17-24 展示了如何通过在定义三个 future 的地方调用 pin! 宏并调整 trait 对象类型来修复代码。
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.
extern crate trpl; // required for mdbook test
use std::pin::{Pin, pin};
// --snip--
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let rx_fut = pin!(async {
// --snip--
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
let tx_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
});
}
这个示例现在可以编译运行了,我们可以在运行时从 vector 中添加或移除 future,并将它们全部连接起来。
This example now compiles and runs, and we could add or remove futures from the vector at runtime and join them all.
Pin 和 Unpin 主要对于构建底层库或构建运行时本身很重要,而不是对于日常的 Rust 代码。但是,当你以后在错误消息中看到这些 trait 时,你就会更清楚如何修复你的代码了!
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!
注意:
Pin和Unpin的这种结合使得在 Rust 中安全地实现一整类复杂类型成为可能,否则这些类型由于自引用而极具挑战性。如今需要Pin的类型最常见于异步 Rust,但偶尔你可能也会在其他上下文中看到它们。
Note: This combination of
PinandUnpinmakes 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 requirePinshow up most commonly in async Rust today, but every once in a while, you might see them in other contexts, too.
Pin 和 Unpin 的具体工作方式,以及它们必须遵守的规则,在 std::pin 的 API 文档中有详尽介绍,如果你有兴趣了解更多,那是开始学习的好地方。
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.
如果你想更详细地了解底层的运作方式,请参阅《Rust 异步编程》的第 2 章和第 4 章。
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 Trait
The Stream Trait
现在你对 Future、Pin 和 Unpin trait 有了更深入的了解,我们可以将注意力转向 Stream trait。正如你本章前面学到的,流类似于异步迭代器。然而,与 Iterator 和 Future 不同的是,截至目前,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 trait 如何合并 Iterator 和 Future 之前,让我们先回顾一下它们的定义。从 Iterator 中,我们得到了序列的概念:它的 next 方法提供一个 Option<Self::Item>。从 Future 中,我们得到了随时间变化的就绪概念:它的 poll 方法提供一个 Poll<Self::Output>。为了表示随时间变得就绪的项序列,我们定义了一个结合了这些特性的 Stream trait:
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 trait 为流产生的项的类型定义了一个名为 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 那样产生项序列。它的返回类型结合了 Poll 和 Option。外层类型是 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!
然而,在“Streams:顺序运行的 Future”一节中看到的示例中,我们没有使用 poll_next 或 Stream,而是使用了 next 和 StreamExt。当然,我们 可以 通过手写自己的 Stream 状态机来直接根据 poll_next API 进行操作,就像我们 可以 通过 poll 方法直接处理 future 一样。但使用 await 要美妙得多,而 StreamExt trait 提供了 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() {
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>>;
}
trait StreamExt: Stream {
async fn next(&mut self) -> Option<Self::Item>
where
Self: Unpin;
// other methods...
}
}
注意:我们在本章前面使用的实际定义看起来与此略有不同,因为它支持还不支持在 trait 中使用异步函数的 Rust 版本。因此,它看起来像这样:
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;那个
Next类型是一个实现了Future的struct,它允许我们使用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
Nexttype is astructthat implementsFutureand allows us to name the lifetime of the reference toselfwithNext<'_, Self>, so thatawaitcan work with this method.
StreamExt trait 也是所有可用于流的有趣方法的归宿。StreamExt 会自动为每个实现 Stream 的类型实现,但这些 trait 是分开定义的,以便社区能够迭代便捷 API 而不影响基础 trait。
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 版本中,该 trait 不仅定义了 next 方法,还提供了一个 next 的默认实现,该实现正确处理了调用 Stream::poll_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.
这就是我们将涵盖的关于这些 trait 的底层细节的全部内容。最后,让我们考虑一下 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!