Compare commits

...

2 Commits

Author SHA1 Message Date
KaiserY
e57d9db2d8 update ch17-03 2024-10-29 20:07:09 +08:00
KaiserY
fdcc2a9d03 update ch17-02 2024-10-29 20:03:53 +08:00
2 changed files with 51 additions and 12 deletions

View File

@ -172,21 +172,21 @@ hi number 9 from the first task!
</figure> </figure>
除了发送消息之外,我们需要接收它们。在这个例子中我们可以手动接收,就是调用四次 `rx.recv().await`,因为我们知道进来了多少条消息。在现实生活中,我们通常会等待 *未知* 数量的消息。这时我们需要一直等待直到可以确认没有更多消息了为止。 除了发送消息之外,我们需要接收它们。在这个例子中我们可以手动接收,就是调用四次 `rx.recv().await`,因为我们知道进来了多少条消息。然而,在现实世界中,我们通常会等待 *未知* 数量的消息。这时我们需要一直等待直到可以确认没有更多消息了为止。
在示例 16-10 中,我们使用了 `for` 循坏来处理从异步信道接收的所有消息。然而Rust 目前还没有还没有方法*异步* 序列上编写 `for` 循环。为此,我们需要一个我们还没有见过的新循环类型,`while let` 条件循环。`while let` 循环是我们在第六章中见过的 `if let` 结构的循环版本。只要其指定的模式持续匹配循环就会一直执行。 在示例 16-10 中,我们使用了 `for` 循坏来处理从异步信道接收的所有消息。然而Rust 目前还没有在 *异步* 序列上编写 `for` 循环的方法。取而代之的是,我们需要一个我们还没有见过的新循环类型,`while let` 条件循环。`while let` 循环是我们在第六章中见过的 `if let` 结构的循环版本。只要其指定的模式持续匹配循环就会一直执行。
`rx.recv` 调用产生一个 `Future`,我们会 await 它。运行时会暂停 `Future` 直到它就绪。一旦消息到达future 会解析为 `Some(message)`一有消息到达就会生成。当信道关闭时,不管是否有 *任何* 消息到达future 都会解析为 `None` 来表明没有更多的值了,我们也就应该停止轮询,也就是停止等待。 `rx.recv` 调用产生一个 `Future`,我们会 await 它。运行时会暂停 `Future` 直到它就绪。一旦消息到达future 会解析为 `Some(message)`每次消息到达时都会如此。。当信道关闭时,不管是否有 *任何* 消息到达future 都会解析为 `None` 来表明没有更多的值了,我们也就应该停止轮询,也就是停止等待。
`while let` 循环完成上面说的所有工作。如果 `rx.recv().await` 调用的结果是 `Some(message)`,我们会得到消息并可以在循环体中使用它,就像使用 `if let` 一样。如果结果是 `None`,则循环停止。每次循环执行完毕,它会再次触发 await point如此运行时会再次暂停直到另一条消息到达。 `while let` 循环将上述逻辑整合在一起。如果 `rx.recv().await` 调用的结果是 `Some(message)`,我们会得到消息并可以在循环体中使用它,就像使用 `if let` 一样。如果结果是 `None`,则循环停止。每次循环执行完毕,它会再次触发 await point如此运行时会再次暂停直到另一条消息到达。
现在代码可以成功发送和接收所有的消息了。不幸的是这里还有一些问题。首先消息并不是按照半秒的间隔到达的。它们在程序启动后两秒2000 毫秒)后立刻一起到达。其次,程序永远不会退出!相反它会永远等待新消息。你会需要使用 <span class="keystroke">ctrl-c</span> 来关闭它。 现在代码可以成功发送和接收所有的消息了。不幸的是这里还有一些问题。首先消息并不是按照半秒的间隔到达的。它们在程序启动后两秒2000 毫秒)后立刻一起到达。其次,程序永远不会退出!相反它会永远等待新消息。你会需要使用 <span class="keystroke">ctrl-c</span> 来关闭它。
让我们开始理解为何消息在全部延迟后立刻一起到达,而不是个在延迟后到达。在一个给定的异步代码块,`await` 关键字在代码中出现的顺序也就是程序执行时其发生的顺序。 让我们开始理解为何消息在全部延迟后立刻一起到达,而不是个在延迟后到达。在一个给定的异步代码块,`await` 关键字在代码中出现的顺序也就是程序执行时其发生的顺序。
示例 17-10 中只有一个异步代码块,所以所有的代码线性地执行。这里仍然没有并发。所有`tx.send` 调用交叉着所有的 `trpl::sleep` 调用和它们关联的 await point 一起发生。只有在此之后 `while let` 循环才开始执行 `recv` 调用上的 `await` point。 示例 17-10 中只有一个异步代码块,所以所有的代码线性地执行。这里仍然没有并发。所有 `tx.send` 调用与 `trpl::sleep` 调用及其相关的 await point 是依次进行的。只有在此之后 `while let` 循环才开始执行 `recv` 调用上的 `await` point。
为了得到我们需要的行为,在接收每个消息的休眠延迟发生时,我们需要将 `tx``rx` 操作放置于它们各自的异步代码块中。这样运行时就可以使用 `trpl::join` 来分别执行它们,就像在计数示例中一样。我们再一次 await `trpl::join` 调用的结果,而不是它们各自的 future。如果我们顺序地 await 单个 future则就又回到了一个顺序流这正是我们 *不* 希望做的。 为了得到我们需要的行为,在接收每条消息之间引入休眠延迟,我们需要将 `tx``rx` 操作放置于它们各自的异步代码块中。这样运行时就可以使用 `trpl::join` 来分别执行它们,就像在计数示例中一样。我们再一次 await `trpl::join` 调用的结果,而不是它们各自的 future。如果我们顺序地 await 单个 future则就又回到了一个顺序流这正是我们 *不* 希望做的。
<!-- We cannot test this one because it never stops! --> <!-- We cannot test this one because it never stops! -->
@ -204,7 +204,7 @@ hi number 9 from the first task!
采用示例 17-11 中的更新后的代码,消息会以 500 毫秒的间隔打印,而不是在两秒后就全部一起打印。 采用示例 17-11 中的更新后的代码,消息会以 500 毫秒的间隔打印,而不是在两秒后就全部一起打印。
但是程序仍然永远也不会退出,这是由于 `while let` 循环与 `trpl::join` 的交互方式: 但是程序仍然永远也不会退出,这是由于 `while let` 循环与 `trpl::join` 的交互方式所致
- `trpl::join` 返回的 future 只会完成一次,即传递的 *两个* future 都完成的时候。 - `trpl::join` 返回的 future 只会完成一次,即传递的 *两个* future 都完成的时候。
- `tx` future 在发送 `vals` 中最后一条消息之后的休眠结束后立刻完成。 - `tx` future 在发送 `vals` 中最后一条消息之后的休眠结束后立刻完成。
@ -216,9 +216,9 @@ hi number 9 from the first task!
我们可以在代码的某处调用 `rx.close` 来手动关闭 `rx`,不过这并没有太多意义。在处理了任意数量的消息后停止可以使程序停止,但是可能会丢失消息。我们需要其它的手段来确保 `tx` 在函数的结尾 *之前* 被丢弃。 我们可以在代码的某处调用 `rx.close` 来手动关闭 `rx`,不过这并没有太多意义。在处理了任意数量的消息后停止可以使程序停止,但是可能会丢失消息。我们需要其它的手段来确保 `tx` 在函数的结尾 *之前* 被丢弃。
目前发送消息的异步代码块只是借用了 `tx`,因为发送消息并不需要其所有权,但是如果我们可以将 `tx` 移动move进异步代码快它会在代码块结束后立刻被丢弃。在第十三章中我们学习了如何在闭包上使用 `move` 关键字,在第十六章中,我们知道了使用线程时经常需要移动数据进闭包。同样基本原理也适用于异步代码块,所以 `move` 关键字也能像闭包那样作用于异步代码块。 目前发送消息的异步代码块只是借用了 `tx`,因为发送消息并不需要其所有权,但是如果我们可以将 `tx` 移动move进异步代码快它会在代码块结束后立刻被丢弃。在第十三章中我们学习了如何在闭包上使用 `move` 关键字,在第十六章中,我们知道了使用线程时经常需要移动数据进闭包。同样的基本原理也适用于异步代码块,因此 `move` 关键字也能像闭包那样作用于异步代码块。
在示例 17-12 中,我们将发送消息的异步代码块从 `async` 代码块修改为 `async move` 代码块。当运行 *这个* 版本的代码时,它会在发送和接收完最后一条消息后优雅地关闭。 在示例 17-12 中,我们将发送消息的异步代码块从普通的 `async` 代码块修改为 `async move` 代码块。当运行 *这个* 版本的代码时,它会在发送和接收完最后一条消息后优雅地关闭。
<figure class="listing"> <figure class="listing">
@ -232,6 +232,39 @@ hi number 9 from the first task!
</figure> </figure>
这个异步信道也是一个多生产者信道,所以如果希望从多个 future 发送消息可以调用 `tx` 上的 `clone` 方法。在示例 17-13 中,我们克隆了 `tx`,在第一个异步代码块外面创建 `tx1`。我们像第一个 `tx` 一样将 `tx1` 移动进代码块。接,将原始的 `tx` 移动进一个 *新的* 异步代码块,其中会用一个稍微更长的延迟发送更多的消息。我们碰巧将新代码块放在接收消息的异步代码块之后,不过也可以放在之前。关键的是 future 的 await 的顺序,而不是它们创建的顺序。 这个异步信道也是一个多生产者信道,所以如果希望从多个 future 发送消息可以调用 `tx` 上的 `clone` 方法。在示例 17-13 中,我们克隆了 `tx`,在第一个异步代码块外面创建 `tx1`。我们像第一个 `tx` 一样将 `tx1` 移动进代码块。接下来,将原始的 `tx` 移动进一个 *新的* 异步代码块,其中会用一个稍微更长的延迟发送更多的消息。我们碰巧将新代码块放在接收消息的异步代码块之后,不过也可以放在之前。关键在于 future 被 await 的顺序,而不是它们创建的顺序。
两个发送消息的异步代码块需要是 `async move` 代码块,如此 `tx``tx1` 都会在代码块结束后被丢弃。否则我们就会陷入到开始时同样的无限循环。最后,我们从 `trpl::join` 切换到 `trpl::join3` 来处理额外的 future。
<figure class="listing">
<span class="file-name">文件名src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch17-async-await/listing-17-13/src/main.rs:here}}
```
<figcaption>示例 17-13通过多个异步代码块使用多个发送者</figcaption>
</figure>
现在我们会看到所有来在两个发送 future 的消息。因为发送 future 采用了稍微不同的发送延迟,消息也会以这些不同的延迟接收。
<!-- Not extracting output because changes to this output aren't significant;
the changes are likely to be due to the threads running differently rather than
changes in the compiler -->
```text
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
```
这是一个良好的开始,不过它将我们限制到少数几个 future`join` 两个,或者 `join3` 三个。让我们看下如何处理更多的 future。
[streams]: ch17-05-streams.html

View File

@ -1 +1,7 @@
## 使用任意数量的 futures ## 使用任意数量的 futures
> [ch17-03-more-futures.md](https://github.com/rust-lang/book/blob/main/src/ch17-03-more-futures.md)
> <br>
> commit 9e85fcc9938e8f8c935d0ad8b4db7f45caaa2ca4
当我们在上一部分从使用两个 future 到三个 future 的时候,我们也必须从使用 `join` 切换到 `join3`