Compare commits

...

2 Commits

Author SHA1 Message Date
kazeno
f6f7843eb4 update ch17-06 2025-05-06 18:59:54 +08:00
kazeno
7c61bc4f4b update ch17-05 2025-05-06 16:04:39 +08:00
2 changed files with 36 additions and 10 deletions

View File

@ -220,9 +220,9 @@ pub trait Future {
### `Stream` trait
现在你对 `Future`、`Pin` 和 `Unpin` trait 有更深刻的理解了,我们可以将注意力转向 `Stream` trait。如你在本章之前所学的流类似于异步迭代器。但是不同于 `Iterator``Future`当前本书编写时 `Stream` 在标准库中并无定义,不过在 `futures` crate 中**有**一个很常用的定义被用于整个生态系统
现在你对 `Future`、`Pin` 和 `Unpin` trait 有更深刻的理解了,我们可以将注意力转向 `Stream` trait。如你在本章之前所学的流类似于异步迭代器。但是不同于 `Iterator``Future`截至撰写本书时 `Stream` 在标准库中并无定义,不过在 `futures` crate 中**存在**一个在整个生态系统中广泛使用的常见定义
在学习 `Stream` trait 如何能够将 `Iterator``Future` trait 结合在一起之前,让我们评审一下 `Iterator``Future` trait 的定义。从 `Iterator` 中我们学习到序列的概念:其 `next` 方法提供一个 `Option<Self::Item>`。从 `Future` 中我们学习到随时间就绪的概念:其 `poll` 方法提供一个 `Poll<Self::Output>`。为了表示一个随着时间就绪的项的序列,我们定义了一个将这些功能结合到一起的 `Stream` trait
在学习 `Stream` trait 如何能够将 `Iterator``Future` trait 结合在一起之前,让我们先回顾一下它们的定义。从 `Iterator` 中我们引入了序列的概念:其 `next` 方法提供一个 `Option<Self::Item>`。从 `Future` 中我们学习到随时间就绪的概念:其 `poll` 方法提供一个 `Poll<Self::Output>`。为了表示一个随着时间就绪的项的序列,我们定义了一个将这些功能结合到一起的 `Stream` trait
```rust
use std::pin::Pin;
@ -238,11 +238,11 @@ trait Stream {
}
```
`Stream` trait 定义了一个名为 `Item` 的关联类型来作为流所产生项的类型。这类似于 `Iterator`,其中可能含有零个到多个项,同时不同于 `Future`,它总是有一个单独的 `Output`,即便是 unit 类型 `()`
`Stream` trait 定义了一个名为 `Item` 的关联类型来作为流所产生项的类型。这类似于 `Iterator`,其中可能含有零个到多个项,而有别于 `Future`,后者总是只有一个 `Output`,即使它是 unit 类型 `()`
`Stream` 也定义了一个获取这些项的方法。我们称之`poll_next`,来明确它以 `Future::poll` 同样的方式轮询并以 `Iterator::next` 同样的方式产生一系列的项。其返回类型用 `Option`组合了 `Poll`。外部类型是 `Poll`,因为它必须检查可用性,就像 future 一样。内部类型是 `Option`,因为它需要表明是否有更多消息,就像迭代器一样。
`Stream` 也定义了一个获取这些项的方法。`poll_next`,来明确它以 `Future::poll` 同样的方式轮询并以 `Iterator::next` 同样的方式产生一系列的项。其返回类型用 `Option` 组合了 `Poll`。外部类型是 `Poll`,因为它必须检查可用性,就像 future 一样。内部类型是 `Option`,因为它需要表明是否有更多消息,就像迭代器一样。
一些非常类似这个定义的代码最终非常可能成为 Rust 标准库的一部分。目前,它是大部分运行时工具箱的一部分,所以你可以依赖它,并且接下来所讲一切应该也是适用的!
与此定义非常相似的实现很可能最终会成为 Rust 标准库的一部分。目前,它是大部分运行时工具箱的一部分,所以你可以依赖它,并且接下来所讲一切应该也是适用的!
不过,在这一部分我们之前见过的关于流的示例中,我们没有使用 `poll_next` **或** `Stream`,相反我们使用了 `next``StreamExt`。当然,我们**可以**通过手写自己的 `Stream` 状态机来直接处理 `poll_next` API就像**可以**通过 `poll` 方法直接处理 future 一样。不过,使用 `await` 更加优雅,同时 `StreamExt` trait 提供了 `next` 方法以便我们可以这样做:
@ -250,7 +250,7 @@ trait Stream {
{{#rustdoc_include ../listings/ch17-async-await/no-listing-stream-ext/src/lib.rs:here}}
```
> 注意:本章之前用到的实际定义与这个看起来稍微有点不同,因为它支持还不支持在 trait 中使用异步函数的 Rust 版本。因此,它看起来像这样:
> 注意:本章之前用到的实际定义与这个看起来略有不同,因为它需要支持还不支持在 trait 中使用异步函数的 Rust 版本。因此,它看起来像这样:
>
> ```rust,ignore
> fn next(&mut self) -> Next<'_, Self> where Self: Unpin;
@ -258,11 +258,11 @@ trait Stream {
>
> `Next` 类型是一个实现了 `Future` 并通过 `Next<'_, Self>` 允许我们命名 `self` 引用生命周期的 `struct`,因此 `await` 可以处理这个方法。
`StreamExt` trait 也是所有可用于流的有趣方法的乐园。`StreamExt` 自动为所有实现了 `Stream` 的方法实现,不过这些 trait 是分别定义的以便社区可以迭代便利的工具而不会影响基础 trait。
`StreamExt` trait 也是所有可用于流的有趣方法所在的 trait。`StreamExt` 自动为所有实现了 `Stream` 的方法实现,不过这些 trait 是分别定义的以便社区可以迭代便利的工具而不会影响基础 trait。
`trpl` crate 所用到的 `StreamExt` 版本中,该 trait 不仅定义了 `next` 方法而且提供了一个正确处理 `Stream::poll_next` 细节的 `next` 方法默认实现。这意味着即便当你编写自己的流数据类型时,**只需**实现 `Stream`,接着任何使用你数据类型的人就自动地可以使用 `StreamExt` 其方法。
`trpl` crate 所用到的 `StreamExt` 版本中,该 trait 不仅定义了 `next` 方法而且提供了一个正确处理 `Stream::poll_next` 细节的 `next` 方法默认实现。这意味着即便当你编写自己的流数据类型时,**只需**实现 `Stream`,接着任何使用你数据类型的人就自动地可以使用 `StreamExt` 其方法。
这就是我们要涉及的这些 trait 的底层细节的全部了。作为总结,让我们考虑一下如何将 future包括流、任务和线程全部结合在一起
这就是我们要涉及的这些 trait 的底层细节的全部了。最后,让我们来思考 futures包括 streams、任务和线程如何协同配合
[ch-18]: ch18-00-oop.html
[async-book]: https://rust-lang.github.io/async-book/

View File

@ -4,7 +4,33 @@
> <br>
> commit 06d73f3935dfec895aec9790127dc8b6fc827ce1
正如我们在[第十六章][ch16]所见,线程提供了一种并发的手段。
正如我们在[第十六章][ch16]所见,线程提供了一种并发的方式。在这一章节我们见过了另一种方式:通过 future 和流来使用异步。如果你好奇何时选择一个而不是另一个,答案是:因情况而异!同时在很多场景下,选择不是线程**或**异步而是线程**和**异步。
几十年来很多操作系统已经提供了基于线程的并发模型,因此很多编程语言也支持它们。然而这些模型并非没有取舍。在很多操作系统中,它们为每一个线程使用了不少的内存,同时启动和停止带来了一些开销。线程也只有当你的操作系统和硬件支持它们的时候才是一个选项。不同于主流的台式机和移动电脑,一些嵌入式系统完全没有操作系统,所以它们也没有线程。
异步模型提供了一个不同的 -- 最终也是互补的 -- 权衡取舍。在异步模型中,并发操作并不需求自己的线程。相反,它们运行在任务上,正如流小节中我们用 `trpl::spawn_task` 从异步函数中开始工作一样。任务类似于线程,不过不同于由操作系统管理,它由库级别的代码管理:也就是运行时。
在上一小节,我们看到可以通过异步信道来构建一个流并产生一个可以在异步代码中调用的异步任务。我们也可以用线程来做到完全相同的事情。在示例 17-40 中使用了 `trpl::spawn_task``trpl::sleep`。在示例 17-41 中,我们将 `get_intervals` 函数中的代码替换为标准库中的 `thread::spawn``thread::sleep` API。
<figure class="listing">
<span class="file-name">文件名src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch17-async-await/listing-17-41/src/main.rs:threads}}
```
<figcaption>示例 17-41`get_intervals` 函数使用 `std::thread` API 而不是异步 `trpl` API</figcaption>
</figure>
如果你运行这段代码,其输入与示例 17-40 的一样。并且请注意从调用代码的角度来说改变是多么的微小。而且,即便一个函数在运行时上产生一个异步任务而另一个产生一个系统线程,其返回的流不受该区别的影响。
尽管它们是相似的,这两种方式的行为非常不同,尽管在这个非常简单的例子中我们可能很难进行测量。我们可以在任意现代计算机中产生数以百万计的异步任务。如果尝试用线程来这样做,我们实际上会耗尽内存!
然而,它们的 API 如此相似是有理由的。线程作为同步操作集的边界;线程**之间**的并发是可能的。任务作为**异步**操作集的边界,任务**之间**和**之内**的并发是可能的,因为任务可以在其内部切换 future。最后future 是 Rust 中最细粒度的并发单位,同时每一个 future 可能代表一个其它 future 的数。其运行时 -- 更准确地说其执行器executor-- 管理任务,任务则管理 future。在这一点上任务类似于轻量的、运行时管理的线程并带有额外的由运行时管理而不是操作系统管理的能力。
这并不意味着异步任务总是优于线程(或者相反)。基于线程的并发在某种程度上来说是一个比基于 `async` 的并发更简单的编程模型。这可以是一个优点也可以是一个缺点。线程有点像 “射后不理”“fire and forget”
[ch16]: ch16-00-concurrency.html
[combining-futures]: ch17-03-more-futures.html#构建我们自己的异步抽象