mirror of
https://github.com/gnu4cn/rust-lang-zh_CN.git
synced 2026-08-19 04:33:27 +08:00
Updated 'src/async/async_traits.md'.
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use std::pin::{Pin, pin};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let fut = async {
|
let fut = async {
|
||||||
let (tx, mut rx) = trpl::channel();
|
let (tx, mut rx) = trpl::channel();
|
||||||
|
|
||||||
let tx1 = tx.clone();
|
let tx1 = tx.clone();
|
||||||
let tx1_fut = async move {
|
let tx1_fut = pin! (async move {
|
||||||
let vals = vec![
|
let vals = vec![
|
||||||
String::from("hi"),
|
String::from("hi"),
|
||||||
String::from("from"),
|
String::from("from"),
|
||||||
@@ -17,15 +18,15 @@ fn main() {
|
|||||||
tx1.send(val).unwrap();
|
tx1.send(val).unwrap();
|
||||||
trpl::sleep(Duration::from_millis(500)).await;
|
trpl::sleep(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
let rx_fut = async {
|
let rx_fut = pin! (async {
|
||||||
while let Some(value) = rx.recv().await {
|
while let Some(value) = rx.recv().await {
|
||||||
println!("收到 '{value}'");
|
println!("收到 '{value}'");
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
let tx_fut = async move {
|
let tx_fut = pin! (async move {
|
||||||
let vals = vec![
|
let vals = vec![
|
||||||
String::from("more"),
|
String::from("more"),
|
||||||
String::from("messages"),
|
String::from("messages"),
|
||||||
@@ -37,9 +38,12 @@ fn main() {
|
|||||||
tx.send(val).unwrap();
|
tx.send(val).unwrap();
|
||||||
trpl::sleep(Duration::from_millis(1500)).await;
|
trpl::sleep(Duration::from_millis(1500)).await;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
trpl::join! (tx1_fut, tx_fut, rx_fut);
|
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
|
||||||
|
vec![tx1_fut, rx_fut, tx_fut];
|
||||||
|
|
||||||
|
trpl::join_all(futures).await;
|
||||||
};
|
};
|
||||||
|
|
||||||
trpl::block_on(fut);
|
trpl::block_on(fut);
|
||||||
|
|||||||
@@ -76,43 +76,59 @@ loop {
|
|||||||
|
|
||||||
## `Pin` 与 `Unpin` 特质
|
## `Pin` 与 `Unpin` 特质
|
||||||
|
|
||||||
回到 [清单 17-13](./concurrency_n_async.md#listing_17-13),我们使用了 `trpl::join!` 宏来等待三个未来值。然而,通常情况下都有着包含着直到运行时才已知数量的未来值的某种集合。
|
回到 [清单 17-13](./concurrency_n_async.md#listing_17-13),我们使用了 `trpl::join!` 宏来等待三个未来值。然而,通常情况下都有着包含直到运行时才已知数量未来值的诸如矢量值的某种集合。我们来修改清单 17-13 为下面清单 17-23 中的代码,放置三个未来值到一个矢量中,并转而调用 `trpl::join_all` 函数,其尚不会编译。
|
||||||
让我们将列表 17-13 修改为列表 17-23 中的代码,该代码将三个未来对象放入一个向量中,并改用 trpl::join_all 函数,不过目前这段代码还无法编译。
|
|
||||||
|
|
||||||
当我们在 [清单 17-16](./multiple_futures.md#listing-17-16) 中引入 “固定” 这个概念时,我们遇到了一个非常棘手的错误消息。下面是其中的相关部分:
|
<a name="listing_17-23"></a>
|
||||||
|
文件名:`src/main.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let tx_fut = async move {
|
||||||
|
// -- 跳过代码 --
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
```
|
||||||
|
|
||||||
|
**清单 17-23**:等待集合中的未来值
|
||||||
|
|
||||||
|
我们放置每个未来值于 `Box` 内,使他们成为 *特质对象*,就像我们在第 12 章中 [返回 `run` 中的错误](../io_project/refactoring.md#返回-run-中的错误) 小节中所做的那样。(我们将在第 18 章中详细介绍特质对象。)使用特质对象让我们可以将这些类型生成的每个匿名未来值视为同一类型,因为他们都实现了 `Future` 特质。
|
||||||
|
|
||||||
|
这可能会令人惊讶。毕竟,没有一个异步代码块返回任何内容,因此每个都会生成一个 `Future<Output = ()>`。但请记住,`Future` 属于特质,编译器会为每个异步代码块创建一个唯一的枚举,即使他们有着相同的输出类型。正如咱们不能放置两个不同的手写结构体于 `Vec` 中一样,咱们也不能混合编译器生成的枚举。
|
||||||
|
|
||||||
|
然后我们传递这个未来值的集合给 `trpl::join_all` 函数并等待结果。然而,这段代码不会编译;以下是错误信息的相关部分。
|
||||||
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned
|
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
|
||||||
--> src/main.rs:48:33
|
--> src/main.rs:45:9
|
||||||
|
|
|
|
||||||
48 | trpl::join_all(futures).await;
|
45 | trpl::join_all(futures).await;
|
||||||
| ^^^^^ the trait `Unpin` is not implemented for `{async block@src/main.rs:10:23: 10:33}`
|
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
||||||
|
|
|
|
||||||
= note: consider using the `pin!` macro
|
= note: consider using the `pin!` macro
|
||||||
consider using `Box::pin` if you need to access the pinned value outside of the current scope
|
consider using `Box::pin` if you need to access the pinned value outside of the current scope
|
||||||
= note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement `Future`
|
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
|
||||||
note: required by a bound in `futures_util::future::join_all::JoinAll`
|
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
|
--> /home/hector/.cargo/registry/src/mirrors.ustc.edu.cn-5857e57f01837ef8/futures-util-0.3.32/src/future/join_all.rs:29:8
|
||||||
|
|
|
|
||||||
27 | pub struct JoinAll<F>
|
27 | pub struct JoinAll<F>
|
||||||
| ------- required by a bound in this struct
|
| ------- required by a bound in this struct
|
||||||
28 | where
|
28 | where
|
||||||
29 | F: Future,
|
29 | F: Future,
|
||||||
| ^^^^^^ required by this bound in `JoinAll`
|
| ^^^^^^ required by this bound in `JoinAll`
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
这一错误消息中的注解告诉我们,应使用 `pin!` 宏来 *固定* 值,这意味着放置他们于 `Pin` 类型内,以保证这些值不会在内存中迁移。错误消息指出需要固定,因为 `dyn Future<Output = ()>` 需要实现 `Unpin` 特质,而他目前尚未实现。
|
||||||
|
|
||||||
这条错误消息不仅告诉我们需要固定这些值,还告诉我们为什么需要固定。`trpl::join_all` 这个函数会返回一个名为 `JoinAll` 的结构体。该结构体是个 `F` 类型的泛型,而 `F` 类型被约束到实现 `Future` 这个特质。以 `await` 直接等待某个未来值,就会隐式地固定这个未来值。这就是我们不需要在所有我们需要等待未来值的地方,使用 `pin!` 的原因。
|
`trpl::join_all` 函数返回一个名为 `JoinAll` 的结构体。该结构体对类型 `F` 是泛型的,被约束为实现 `Future` 特质。以 `await` 直接等待未来值,会隐式地固定该未来值。这就是为什么我们不需要在等待未来值的每个地方都使用 `pin!`。
|
||||||
|
|
||||||
|
不过,我们在这里并不是直接等待未来值。相反,我们通过传递一个未来值集合给 `join_all` 函数,构造了一个新的未来值 `JoinAll`。`join_all` 的签名要求集合中项目的类型都实现 `Future` 特质,而仅当他所封装的 `T` 是个实现 `Unpin` 特质的未来值时,`Box<T>` 才实现 `Future` 特质。
|
||||||
|
|
||||||
不过,我们并没有在这里直接等待某个未来。相反,通过向 `join_all` 函数传递一个未来值的集合,我们构建了一个新未来值,即 `JoinAll`。`join_all` 的函数签名,要求该集合中所有项目的类型,都要实现了 `Future` 特质,而只有当取封装的 `T` 是个实现了 `Unpin` 特质的未来值时,`Box<T>` 才实现了 `Future`。
|
这确实有很多内容需要消化!为了真正理解这一点,我们来进一步深入了解 `Future` 特性的实际工作原理,特别是在固定方面。请再次查看 `Future` 特质的定义:
|
||||||
|
|
||||||
|
|
||||||
要理解的东西还真不少!为了真正理解他,我们来进一步深入了解 `Future` 这个特质的具体工作原理,尤其是与 *固定* 有关的部分。
|
|
||||||
|
|
||||||
|
|
||||||
再看看这个 `Future` 特质的定义:
|
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -122,175 +138,123 @@ use std::task::{Context, Poll};
|
|||||||
pub trait Future {
|
pub trait Future {
|
||||||
type Output;
|
type Output;
|
||||||
|
|
||||||
// Required method
|
// 必需方法
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
其中的 `cx` 参数及其 `Context`类型,是运行时在保持懒惰的同时,如何具体知道何时检查任何给定未来值的关键。同样,如何工作的细节,超出了本章的讨论范围,咱们通常只有在编写某个定制的 `Future` 实现时,才需要考虑这个问题。我们将重点关注 `self` 的类型,因为这是我们第一次看到某个方法中 `self` 有着类型注解。`self` 的类型注解,与其他函数参数的类型注解一样,但有两个关键区别:
|
其中 `cx` 参数及其 `Context` 类型,是运行时在保持惰性计算的同时,实际判断何时检查给定 `Future` 的关键。同样,其具体工作原理超出了本章的讨论范围,咱们通常只有在编写定制 `Future` 实现时,才需要考虑这一点。我们转而将重点关注 `self` 的类型,因为这是我们第一次看到其中 `self` 有着类型注解的方法。 `self` 的类型注解,与其他函数参数的类型注解原理相同,但有两个关键区别:
|
||||||
|
|
||||||
|
- 他告诉 Rust,对于要调用的方法 `self` 必须是什么类型;
|
||||||
|
+ 他不能只是任意类型。他仅限于
|
||||||
|
- 方法实现所基于的类型、
|
||||||
|
- 到该类型的引用或灵巧指针,
|
||||||
|
- 或者封装了到该类型的引用的 `Pin`。
|
||||||
|
|
||||||
|
|
||||||
- 他告诉 Rust,要调用方法的 `self` 必须是何类型;
|
我们将在 [第 18 章](../Ch17_Object_Oriented_Programming_Features_of_Rust.md) 中看到有关这一语法的更多内容。目前只需知道,当我们打算轮询某个未来值,来检查他处于 `Pending` 还是 `Ready(Output)` 状态时,就需要一个由 `Pin` 封装的指向该类型的可变引用。
|
||||||
- 他不能是任何类型。他被限制在该方法所实现的类型、指向该类型的引用或灵巧指针,或封装了指向该类型引用的一个 `Pin`。
|
|
||||||
|
`Pin` 是针对 `&`、`&mut`、`Box` 和 `Rc` 等类指针类型的封装器。(从技术上讲,`Pin` 适用于实现 `Deref` 或 `DerefMut` 特质的类型,但这实际上相当于仅适用于引用和灵巧指针。)`Pin` 本身并非指针,也不具备 `Rc` 和 `Arc` 那样带有引用计数的自身行为;他纯粹是编译器用来强制执行指针使用约束的工具。
|
||||||
|
|
||||||
|
回顾一下,`await` 是通过调用 `poll` 来实现的,这开始解释了我们之前看到的错误信息,但错误消息提到 `Unpin`,而不是 `Pin`。那么,`Pin` 与 `Unpin` 究竟有何关联?为什么 `Future` 需要 `self` 属于 `Pin` 类型才能调用 `poll`?
|
||||||
|
|
||||||
|
还记得这一章前面的内容吗?某个未来值中的一系列等待点,会被编译成一个状态机,而编译器会确保该状态机遵循 Rust 所有关于安全性的正常规则,包括借用和所有权。为了实现这一点,Rust 会分析从一个等待点到下一个等待点,或到异步代码块结束处之间所需的数据。随后,他会在编译后的状态机中创建相应的变体。每个变体都会获得对将用于源代码中该小节的数据的所需权限,无论是通过获取该数据的所有权,还是通过获取对其的可变或不可变引用。
|
||||||
|
|
||||||
|
到目前为止一切顺利:当我们在给定异步代码块中的所有权或引用方面有任何错误之处,借用检查器都将告诉我们。但当我们迁移与该代码块对应的未来值时 —— 比如迁移他到 `Vec` 中以传递给 `join_all` —— 情况就会变得棘手。
|
||||||
|
|
||||||
|
当我们迁移未来值时 —— 无论是压入其到数据结构中,作为用作与 `join_all` 一起使用的迭代器,还是从函数中返回他 —— 这实际上意味着迁移 Rust 为我们创建的状态机。而且与 Rust 中的大多数其他类型不同,Rust 为异步代码块创建的未来值,最终会成为在某个变体的字段中对自身的引用,如下图 17-4 中的简化示意图所示。
|
||||||
|
|
||||||
|
<a name="f_17-4"></a>
|
||||||
|

|
||||||
|
|
||||||
|
**图 17-4**:自引用数据类型
|
||||||
|
|
||||||
|
|
||||||
我们将在 [第 18 章](../Ch17_Object_Oriented_Programming_Features_of_Rust.md) 详细介绍这种语法。现在,我们只需知道,在咱们打算轮询某个未来值,检查他是 `Pending` 还是 `Ready(Output)` 时,我们需要一个指向该类型的 `Pin` 封装的可变引用。
|
但在默认情况下,任何有着对自身的引用的对象,在迁移时都是不安全的,因为引用总是指向其引用对象的实际内存地址(见图 17-5)。当咱们迁移数据结构本身时,这些内部引用将保留指向旧的位置。然而,该内存位置现在是无效的。一方面,当咱们修改数据结构时,其值将不会得以更新。更重要的是,计算机现在可以自由地将该内存用于其他目的!咱们最终会读取到完全无关的数据。
|
||||||
|
|
||||||
`Pin` 是个对诸如 `&`、`&mut`、`Box` 及 `Rc` 等指针类型的封装器。(技术上讲,`Pin` 可以与实现了 `Deref` 或 `DerefMut` 特质的类型一起工作,不过这实际上等同于只与指针一起工作。)`Pin` 本身并非指针,也不像带有引用计数的 `Rc` 与 `Arc` 那样,有任何其自身的行为;他纯粹是个编译器可用于强制约束指针使用的工具。
|
<a name="f_17-5"></a>
|
||||||
|

|
||||||
|
|
||||||
|
**图 17-5**:迁移自引用数据类型的不安全结果
|
||||||
|
|
||||||
|
理论上,Rust 编译器可以尝试在对象被迁移时更新指向该对象的所有引用,但这会增加大量性能开销,尤其是当整个引用网络需要更新时。当我们转而可以确保相关的数据结构在内存中 *不会被迁移* 时,我们将不必更新任何引用。这正是 Rust 的借用检查器的作用:在安全的代码中,他会阻止咱们迁移带有活动引用引用的项目。
|
||||||
|
|
||||||
|
`Pin` 在此基础上给予我们所需的确切保证。当我们通过封装指向该值的指针在 Pin 中,*固定* 某个值时,其便无法再被迁移。因此,当咱们有着 `Pin<Box<SomeType>>` 时,咱们实际上固定了 `SomeType` 值,而 *不是* `Box` 指针。下图 17-6 演示这一过程。
|
||||||
|
|
||||||
|
<a name="f_17-6"></a>
|
||||||
|

|
||||||
|
|
||||||
|
**图 17-6**:固定某个 `Box`,其指向某个自引用的未来值类型
|
||||||
|
|
||||||
|
实际上,指针 `Box` 仍然可以自由迁移。请记住:我们关心的是确保最终引用的数据保持在原地。当指针迁移时,*但他所指向的数据* 位于原处,如下图 17-7 中所示,则没有潜在的问题。(作为一项独立练习,请查阅该类型以及 `std::pin` 模组的文档,并尝试弄清楚咱们如何以封装 `Box` 的 `Pin` 来实现这点。)关键在于,自引用类型本身无法迁移,因为他仍然被固定着。
|
||||||
|
|
||||||
|
<a name="f_17-7"></a>
|
||||||
|

|
||||||
|
|
||||||
|
**图 17-7**:迁移某个 `Box`,其指向某个自引用的未来值类型
|
||||||
|
|
||||||
|
然而,大多数类型都可以安全地迁移,即使他们恰好位于 `Pin` 指针之后。我们只需在项目有着内部引用时,才需要考虑固定操作。数字和布尔值等原始值是安全的,因为他们显然没有任何内部引用。咱们在 Rust 中通常使用的绝大多数类型也是如此。例如,咱们可以放心迁移 `Vec`。根据我们迄今所见,当咱们有个 `Pin<Vec<String>>` 时,就必须通过 `Pin` 提供的安全但受限的 API 执行所有操作,即使在没有其他引用时 `Vec<String>` 始终可以安全地迁移。我们需要一种方式来告诉编译器,在这种情况下迁移项目是安全的 —— 而这就是 `Unpin` 发挥作用的地方。
|
||||||
|
|
||||||
|
`Unpin` 是个标记特质,类似于我们在第 16 章中看到的 `Send` 和 `Sync` 特质,而因此本身不具备任何功能。标记特质的存在只是为了告知编译器,在特定上下文中使用实现给定特质的类型是安全的。`Unpin` 通知编译器,给定类型 *不* 需要对相关值是否可以安全移动提供任何保证。
|
||||||
|
|
||||||
|
就像 `Send` 和 `Sync` 一样,编译器会自动为所有能证明其安全的类型实现 `Unpin`。与 `Send` 和 `Sync` 类似的一种特殊情况是,`Unpin` 并 *未* 针对某种类型实现。其表示法为 `impl !Unpin for SomeType`,其中 `SomeType` 是类型的名称,每当在 `Pin` 中使用指向该类型的指针时,该类型都 *必须* 遵守这些保证才能确保安全。
|
||||||
|
|
||||||
|
换句话说,关于 `Pin` 和 `Unpin` 之间的关系,有两点需要记住。首先,`Unpin` 属于 “正常” 情况,而 `!Unpin` 属于特殊情况。其次,只有在咱们使用指向该类型的固定指针(如 `Pin<&mut SomeType>`)时,类型是实现了 `Unpin` 还是 `!Unpin` 才重要。
|
||||||
|
|
||||||
|
为了具体说明这一点,请考虑一个 `String`:他有着一个长度以及构成他的 Unicode 字符。我们可以像下图 17-8 中所示那样,封装 `String` 在 `Pin` 中。然而,`String` 会自动实现 `Unpin`,正如 Rust 中大多数其他类型那样。
|
||||||
|
|
||||||
|
<a name="f_17-8"></a>
|
||||||
|

|
||||||
|
|
||||||
|
**图 17-8**:固定一个 `String`;虚线表示 `String` 实现了 `Unpin` 特质,而因此未被固定
|
||||||
|
|
||||||
|
因此,我们可以执行一些当 `String` 实现了 `!Unpin` 时不合法的操作,比如下图 17-9 中所示,在内存中完全相同的位置以一个字符串替换另一个字符串。这并不违反 `Pin` 的合约,因为 `String` 没有使迁移不安全的内部引用。这正是他实现 `Unpin` 而非 `!Unpin` 的原因。
|
||||||
|
|
||||||
|
|
||||||
回顾 `await` 是通过到 `poll` 的调用实现的,这就可以解释我们早先看到的错误消息,但那是指 `Unpin` 而不是 `Pin`。那么 `Pin` 与 `Unpin` 到底有什么关系,为什么 `Future` 需要 `self` 位于某个 `Pin` 类型中,才能调用 `poll` 呢?
|
<a name="f_17-9"></a>
|
||||||
|

|
||||||
|
|
||||||
|
**图 17-9**:在内存中以一个完全不同的 `String` 替换原有的 `String`
|
||||||
|
|
||||||
还记得在本章前面的内容中,在某个未来值中的一系列等待点,都会被编译到一个状态机中,且编译器会确保该状态机遵循 Rust 有关安全性的所有一般规则,包括借用及所有权。为实现这一点,Rust 会查看从一个等待点,与下一等待点或异步代码块结束处间,需要哪些数据。然后他会在编译后的状态机中,创建相应变种。无论是通过获取该数据的所有权,还是通过获取数据的可变或不可变引用,每个变种都会获得,其所需的对将在源代码该部分中用到数据的访问权。
|
现在我们已经掌握了足够的知识,可以理解 [清单 17-23](#listing_17-23) 中 `join_all` 调用所报告的错误了。我们最初尝试迁移异步代码块生成的未来值到 `Vec<Box<dyn Future<Output = ()>>>` 中,但正如我们所见,这些未来值可能有着内部引用,因此他们没有自动实现 `Unpin`。 一旦我们固定他们,我们就可以传递生成的 `Pin` 类型给 `Vec`,并确信未来值中的底层数据不会被迁移。下面清单 17-24 展示了如何通过在三个未来值各自的定义处,调用 `pin!` 宏并调整特质对象类型来修复代码。
|
||||||
|
|
||||||
|
<a name="listing_17-24"></a>
|
||||||
|
```rust
|
||||||
|
use std::pin::{Pin, pin};
|
||||||
|
|
||||||
到目前为止,一切都很好:当我们在某个给定异步代码块中的所有权或引用上有任何错误时,借用检查器就将告诉我们。当我们要绕过与该代码块对应的未来值 -- 比如将其迁移到某个 `Vec` 中,以传递给 `join_all` -- 事情就变得棘手了。
|
// --跳过代码--
|
||||||
|
|
||||||
|
let tx1_fut = pin!(async move {
|
||||||
|
// --跳过代码--
|
||||||
|
});
|
||||||
|
|
||||||
当我们迁移某个未来值 -- 无论是将其压入某个数据结构,以 `join_all` 用作迭代器,还是从函数中返回他 -- 这实际上意味着迁移 Rust 为我们创建的那个状态机。与 Rust 中的大多数其他类型不同,Rust 为异步代码块创建的未来值,最终会在任何给定变种的字段中,以到他们自身的引用结束,如图 17-4 中的简化插图所示。
|
let rx_fut = pin!(async {
|
||||||
|
// --跳过代码--
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx_fut = pin!(async move {
|
||||||
|
// --跳过代码--
|
||||||
|
});
|
||||||
|
|
||||||

|
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
|
||||||
|
vec![tx1_fut, rx_fut, tx_fut];
|
||||||
|
```
|
||||||
|
|
||||||
|
**清单 17-24**:固定未来值,以便能够迁移他们到矢量中
|
||||||
|
|
||||||
*图 17-4:一种自引用的数据类型*
|
这个示例现在会编译并运行,并且我们可以在运行时向该矢量值添加或移除未来值,并将他们全部连接起来。
|
||||||
|
|
||||||
|
`Pin` 和 `Unpin` 主要对于构建底层库,或者咱们在构建运行时本身时很重要,而非日常的 Rust 代码。但是,当咱们在错误消息中看到这两个特质时,现在咱们将更好地了解,该如何修复代码了!
|
||||||
|
|
||||||
但在默认情况下,任何有着对自身引用的对象,在迁移时都不安全,因为引用总是指向其所引用对象的具体内存地址(见图 17-5)。若咱们迁移该数据结构本身,这些内部引用将仍旧指向旧位置。但是,该内存位置现在是无效的。首先,当咱们更改该数据结构时,他的值不会更新。更重要的是,计算机现在可以将该内存,重新用于其他目的!咱们可能会在最后读到完全无关的数据。
|
> **注意**:`Pin` 和 `Unpin` 的这种组合,使得在 Rust 中安全地实现一整类复杂类型成为可能,否则将具挑战性,因为这些复杂类型属于自引用的。如今,需要 `Pin` 的类型最常见于异步 Rust 中,但偶尔咱们也会在其他场景中见到他们。
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*图 17-5:迁移某个自引用数据类型的不安全结果*
|
|
||||||
|
|
||||||
|
|
||||||
理论上,Rust 编译器可以尝试在对象被迁移时,更新该对象的每个引用,但这会增加大量的性能开销,尤其是当整个引用网络需要更新时。如果我们能确保相关数据结构 *不会在内存中迁移*,我们就不必更新任何引用。这正是 Rust 的借用检查器所要求的:在安全的代码中,他会阻止咱们迁移任何有活动引用的项目。
|
|
||||||
|
|
||||||
|
|
||||||
`Pin` 就是建立在此基础上,为我们提供了我们所需的确切保证。当我们通过将某个到该值的指针,封装在 `Pin` 中,而 *固定* 了某个值时,该值就不再可迁移了。因此,若咱们有着 `Pin<Box<SomeType>>` 时,咱们实际上固定了这个 `SomeType` 的值,而 *不是* 那个 `Box` 指针。图 17-6 演示了这一过程。
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*图 17-6:固定某个指向一个自引用未来值类型的 `Box`*
|
|
||||||
|
|
||||||
|
|
||||||
事实上,这个 `Box` 指针仍然可以自由迁移。请记住:我们关心的是,确保那个最终被引用的数据保持在原处。如果某个指针四处迁移,*但其指向的数据仍在原处*,如图 17-7 所示,就不会有潜在问题。(作为一个独立练习,请查看这些类型及 `std::pin` 这个模块的文档,并尝试解决咱们应如何以封装了某个 `Box` 的 `Pin`,实现这点。)关键在于,自引用类型本身不能迁移,因为他仍被固定着。
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
*图 17-7:迁移指向某个自引用未来值类型的 `Box`*
|
|
||||||
|
|
||||||
|
|
||||||
不过,大多数类型都可以安全地迁移,即使他们碰巧位于某个 `Pin` 封装器之下。只有当项目有着内部引用时,我们才需要考虑固定。诸如数字及布尔值的原生值是安全的,因为他们显然没有任何的内部引用。咱们在 Rust 中用到的大多数类型也是如此。例如,咱们可随意迁移某个 `Vec`,而不必担心。仅就我们目前所见,如果咱们有个 `Pin<Vec<String>>`,咱们就必须通过 `Pin` 提供的安全但限制性的 API,完成所有操作,尽管在没有到其的其他引用下,移动 `Vec<String>` 总是安全的。我们需要一种告诉编译器,在这种情况下移动项目是安全的方式 -- 这就是 `Unpin` 发挥作用的地方。
|
|
||||||
|
|
||||||
|
|
||||||
`Unpin` 是个标记性特质,类似于我们在第 16 章中看到的 [`Send` 与 `Sync` 特质](../concurrency/extensible_concurrency.md),因此本身没有任何功能。标记性特质的存在,只是为了告诉编译器,在特定上下文中使用实现了某个给定特质的类型是安全的。`Unpin` 告知编译器,某个给定类型 *不* 需要对相关值是否可以安全移动做出任何保证。
|
|
||||||
|
|
||||||
|
|
||||||
与 `Send` 与 `Sync` 一样,编译器会为所有能证明其安全的类型,自动实现 `Unpin`。与 `Send` 与 `Sync` 类似的一种特殊情况是,`Unpin` *未* 对某一类型而被实现。这种情况的记法为 `impl !Unpin for SomeType`,其中,`SomeType` 是某个指向该类型的指针被用于某个 `Pin` 中时,*确实* 需要保证安全的类型名字。
|
|
||||||
|
|
||||||
|
|
||||||
换句话说,关于 `Pin` 与 `Unpin` 之间的关系,有两点需要注意。首先,`Unpin` 属于 “正常” 情况,而 `!Unpin` 是特殊情况。其次,只有在使用像是 `Pin<&mut SomeType>` 等指向某个类型的固定指针时,该类型实现 `Unpin` 还是 `!Unpin` *才* 有意义。
|
|
||||||
|
|
||||||
|
|
||||||
为具体说明这点,请设想某个 `String`:他有着一个长度,以及组成他的一些 Unicode 字符。我们可将某个 `String` 封装在某个 `Pin` 中,如图 17-8 所示。然而,与 Rust 中的大多数其他类型一样,`String` 会自动实现 `Unpin`。
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*图 17-8:固定某个 `String`;虚线表示这个 `String` 实现了 `Unpin` 特质,因此其未被固定*
|
|
||||||
|
|
||||||
|
|
||||||
因此,在 `String` 实现了 `!Unpin` 时,我们就可以执行原本非法的一些事情,例如以一个字符串,替换图 17-9 中内存里完全同一位置的另一字符串。这并不违反 `Pin` 的合约,因为 `String` 没有令其迁移不安全的内部引用!这正是他实现 `Unpin` 而不是 `!Unpin` 的原因。
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
*图 17-9:在内存中以一个完全不同的 `String` 替换 `String`*
|
|
||||||
|
|
||||||
|
|
||||||
现在我们知道了理解 [清单 17-17](./multiple_futures.md#listing-17-17) 中,`join_all` 调用所报出错误的足够信息。我们最初尝试将异步代码块产生的未来值,迁移到某个 `Vec<Box<dyn Future<Output = ()>>>` 中,但正如我们所见,这些未来值可能有内部引用,因此他们无法实现 `Unpin`。他们需要被固定下来,然后我们就可以将 `Pin` 类型,传递到 `Vec` 中,确保这些未来值中的底层数据,不会被迁移。
|
|
||||||
|
|
||||||
|
|
||||||
`Pin` 和 `Unpin` 对于构建底层库或,或在咱们构建运行时本身很重要,而不是对日常的 Rust 代码很重要。不过,当咱们在错误消息中看到这些特质时,现在咱们就能更好地知道,如何修复咱们的代码了!
|
|
||||||
|
|
||||||
|
|
||||||
> **注意**:`Pin` 和 `Unpin` 的结合,使得在 Rust 中安全地实现一整类复杂类型成为可能,否则这些复杂类型就会因为自引用而具有挑战性。目前,需要 `Pin` 的类型最常见于异步的 Rust 中,但偶尔咱们也会在其他上下文中看到他们。
|
|
||||||
>
|
>
|
||||||
> `std::pin` 的 API 文档中,详细介绍了 `Pin` 和 `Unpin` 的工作原理,以及他们需要遵守的规则,因此如果咱们有兴趣了解更多,可以从这里开始。
|
> 关于 `Pin` 和 `Unpin` 的具体工作原理,及其必须遵守的规则,在 `std::pin` 的 API 文档中有详尽的说明,因此若咱们打算了解更多,这是个很好的起点。
|
||||||
>
|
>
|
||||||
> 如果咱们想更详细地了解表象之下的工作原理,请参阅 [《Rust 中的异步编程》](https://rust-lang.github.io/async-book/) 的 [第 2 章](https://rust-lang.github.io/async-book/02_execution/01_chapter.html) 和 [第 4 章](https://rust-lang.github.io/async-book/04_pinning/01_chapter.html)。
|
> 若咱们想更深入地了解其底层工作原理,请参阅 [《Rust 异步编程》](https://rust-lang.github.io/async-book/) 的 [第 2 章](https://rust-lang.github.io/async-book/02_execution/01_chapter.html) 和 [第 4 章](https://rust-lang.github.io/async-book/04_pinning/01_chapter.html)。
|
||||||
|
|
||||||
|
|
||||||
## `Stream` 特质
|
## `Stream` 特质
|
||||||
|
|
||||||
|
现在咱们对 `Future`、`Pin` 和 `Unpin` 特质有了更深入的理解,我们就可以转移注意力到 `Stream` 特质了。正如你在这一章前面了解到的,流类似于异步的迭代器。然而,与 Iterator 和 Future 不同的是,截至本文撰写之时,标准库中尚未定义 Stream,但 futures 库中提供了一个非常通用的定义,该定义在整个生态系统中被广泛使用。
|
||||||
|
|
||||||
现在咱们已经对 `Future`、`Pin` 和 `Unpin` 三个特质有了更深入了解,我们可以将注意力转向 `Stream` 这个特质。正如咱们在本章前面所了解的,流类似于异步的迭代器。然而,与 `Iterator` 及 `Future` 不同的是,在本文档编写时,`Stream` 在标准库中还没有定义,但在整个生态系统中用到的 `futures` 代码箱中,*有* 个非常普遍的定义。
|
在探讨 Stream 特质如何将它们融合之前,让我们先回顾一下 Iterator 和 Future 特质的定义。从 Iterator 中,我们获得了序列的概念:其 next 方法返回 Option<Self::Item>。从 Future 中,我们获得了随时间推移而就绪的概念:其 poll 方法返回 Poll<Self::Output>。为了表示随时间推移而就绪的项目序列,我们定义了一个将这些特性结合在一起的 Stream 特质:
|
||||||
|
|
||||||
|
|
||||||
我们来先回顾一下 `Iterator` 及 `Future` 两个特质的定义,然后再看 `Stream` 特质如何将二者合并在一起。从 `Iterator` 中,我们获得了序列的概念:他的 `next` 方法,提供了一个 `Option<Self::Item>` 选项。而从 `Future` 中,我们获得了随时间变化准备状态的概念,the idea of readiness over time:他的 `poll` 方法提供了一个 `Poll<Self::Output>` 。为了表示随时间变化而就绪的一个项目序列,我们定义了一个这两个特质组合在一起的 `Stream` 特质:
|
|
||||||
|
|
||||||
|
|
||||||
```rust
|
|
||||||
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`,即使他是单元值类型 `()`。
|
|
||||||
|
|
||||||
|
|
||||||
`Stream` 还定义了个获取这些项目的方法。我们称其为 `poll_next`,以清楚地表明他会以 `Future::poll` 相同方式轮询,并以 `Iterator::next` 相同方式产生项目序列。其返回类型结合了 `Poll` 与 `Option`。外层类型为 `Poll`,因为与未来值一样,他必须检查就绪状态。内层类型为 `Option`,因为与迭代器一样,他需要发出是否有更多消息的信号。
|
|
||||||
|
|
||||||
|
|
||||||
与这种定义非常接近的定义,很可能最终会成为 Rust 标准库的一部分。在此期间,他是大多数运行时工具套件的一部分,所以咱们可以信赖他,我们接下来要介绍的所有内容,一般都会适用!
|
|
||||||
|
|
||||||
但是,我们在有关流的小节中,所看到示例里,我们并没有使用 `poll_next` 或 `Stream`,而使用了 `next` 和 `StreamExt`。当然,我们 *可* 通过手写我们自己的 `Stream` 状态机,而直接使用 `poll_next` 这个 API,就像我们可经由未来值的 `poll` 方法,直接使用未来值一样。不过,使用 `await` 更好,同时 `StreamExt` 特质提供了 `next` 方法,因此我们才可以这样做:
|
|
||||||
|
|
||||||
|
|
||||||
```rust
|
|
||||||
trait StreamExt: Stream {
|
|
||||||
async fn next(&mut self) -> Option<Self::Item>
|
|
||||||
where
|
|
||||||
Self: Unpin;
|
|
||||||
|
|
||||||
// other methods...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
> **注意**:我们在本章早先用到的具体定义,与此略有不同,因为他支持了那些还不支持在特质中使用异步函数的 Rust 版本。由此,他看起来是这样的:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;
|
|
||||||
```
|
|
||||||
|
|
||||||
> 其中的 `Next` 类型是个实现了 `Future` 的结构体,允许我们将到 `self` 引用的生命周期,命名为 `Next<'_, Self>`,这样 `await` 就可用于这个方法了。
|
|
||||||
|
|
||||||
|
|
||||||
`StreamExt` 特质也是所有可用于流的有趣方法的发源地。每个实现了 `Stream` 的类型,都会自动实现 `StreamExt`,但这两个特质是单独定义的,以便社区在不影响基础特质下,迭代这些方便的 API。
|
|
||||||
|
|
||||||
|
|
||||||
在 `trpl` 这个代码箱中用到的 `StreamExt` 版本中,该特质不仅定义了 `next` 方法,还提供了 `next` 的一种可正确处理调用 `Stream::poll_next` 细节的默认实现。这意味着,即使咱们需要编写咱们自己的流数据类型,咱们也 *只* 需实现 `Stream`,然后用到咱们数据类型的任何人,都可以自动使用 `StreamExt` 及其方法。
|
|
||||||
|
|
||||||
|
|
||||||
以上就是我们要介绍的关于这些特质的底层细节。最后,我们来看看未来值(包括流)、任务及线程,是如何结合在一起的!
|
|
||||||
|
|
||||||
|
|
||||||
(End)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user