check to ch16-03

This commit is contained in:
KaiserY 2018-01-31 21:02:04 +08:00
parent 1b37e6c2f8
commit 185a7d7be4
2 changed files with 68 additions and 51 deletions

View File

@ -4,7 +4,7 @@
> <br> > <br>
> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7 > commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7
最近人气正在上升的一个并发方式是**消息传递***message passing*),这里线程或 actor 通过发送包含数据的消息来沟通。这个思想来源于口号: 一个人气正在上升的确保安全并发的方式是 **消息传递***message passing*),这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 Go 编程语言文档中的口号:
> Do not communicate by sharing memory; instead, share memory by > Do not communicate by sharing memory; instead, share memory by
> communicating. > communicating.
@ -13,11 +13,15 @@
> >
> --[Effective Go](http://golang.org/doc/effective_go.html) > --[Effective Go](http://golang.org/doc/effective_go.html)
实现这个目标的主要工具是**通道***channel*。通道有两部分组成一个发送者transmitter和一个接收者receiver。代码的一部分可以调用发送者和想要发送的数据而另一部分代码可以在接收的那一端收取消息 Rust 中一个实现消息传递并发的主要工具是 **通道***channel*),一个 Rust 标准库提供了其实现的编程概念。你可以将其想象为一个水流的通道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游
我们将编写一个例子使用一个线程生成值并向通道发送他们。主线程会接收这些值并打印出来。
首先,如示例 16-6 所示,先创建一个通道但不做任何事: 编程中的通道有两部分组成一个发送者transmitter和一个接收者receiver。发送者一端位于上游位置在这里可以将橡皮鸭放入河中接收者部分则位于下游橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据另一部分则检查接收端收到到达的消息。当发送者或接收者任一被丢弃时可以认为通道被 **关闭***closed*)了
这里,我们将开发一个程序,它会在一个线程生成值向通道发送,而在另一个线程会接收值并打印出来。这里会通过通道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,就能使用通道来实现聊天系统或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。
首先,在示例 16-6 中,创建了一个通道但没有做任何事。注意这还不能编译,因为 Rust 不知道我们想要在通道中发送什么类型:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -30,13 +34,16 @@ fn main() {
} }
``` ```
<span class="caption">示例 16-6: 创建一个通道,并指派一个包含 `tx``rx` 的元组</span> <span class="caption">示例 16-6: 创建一个通道,并将其两端赋值给 `tx``rx`</span>
`mpsc::channel`函数创建一个新的通道。`mpsc`是**多个生产者,单个消费者***multiple producer, single consumer*)的缩写。简而言之,可以有多个产生值的**发送端**,但只能有一个消费这些值的**接收端**。现在我们以一个单独的生产者开始,不过一旦例子可以工作了就会增加多个生产者。 这里使用 `mpsc::channel` 函数创建一个新的通道;`mpsc` 是 **多个生产者,单个消费者***multiple producer, single consumer*的缩写。简而言之Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的 **发送***sending*)端,但只能有一个消费这些值的 **接收***receiving*)端。想象一下多条小河小溪最终汇聚成大河:所有通过这些小河发出的东西最后都会来到大河的下游。目前我们以单个生产者开始,但是当示例可以工作后会增加多个生产者。
`mpsc::channel`返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,很多人使用`tx`和`rx`作为**发送者**和**接收者**的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个`let`语句和模式来解构了元组。第十八章会讨论`let`语句中的模式和解构。
让我们将发送端移动到一个新建线程中并发送一个字符串,如示例 16-7 所示: <!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->
`mpsc::channel` 函数返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,`tx` 和 `rx` 通常作为 **发送者***transmitter*)和 **接收者***receiver*)的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十八章会讨论 `let` 语句中的模式和解构。如此使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。
让我们将发送端移动到一个新建线程中并发送一个字符串,这样新建线程就可以和主线程通讯了,如示例 16-7 所示。这类似与在和的上游扔下一只橡皮鸭或从一个线程向另一个线程发送聊天信息:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -54,13 +61,13 @@ fn main() {
} }
``` ```
<span class="caption">示例 16-7: 将 `tx` 移动到一个新建的线程中并发送内容 "hi"</span> <span class="caption">示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 “hi”</span>
正如上一部分那样使用`thread::spawn`来创建一个新线程。并使用一个`move`闭包来将`tx`移动进闭包这样新建线程就是其所有者 这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move``tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有通道的发送端以便能向通道发送消息
通道的发送端有一个`send`方法用来获取需要放入通道的值。`send`方法返回一个`Result<T, E>`类型,因为如果接收端被丢弃了,将没有发送值的目标,所以发送操作会出错。在这个例子中,我们简单的调用`unwrap`来忽略错误,不过对于一个真实程序,需要合理的处理它。第九章是你复习正确错误处理策略的好地方 通道的发送端有一个 `send` 方法用来获取需要放入通道的值。`send` 方法返回一个 `Result<T, E>` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。过对于一个真实程序需要合理的处理它回到第九章复习正确处理错误的策略
在示例 16-8 中,我们在主线程中从通道的接收端获取值: 在示例 16-8 中,我们在主线程中从通道的接收端获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -81,19 +88,27 @@ fn main() {
} }
``` ```
<span class="caption">示例 16-8: 在主线程中接收并打印内容 "hi"</span> <span class="caption">示例 16-8: 在主线程中接收并打印内容 “hi”</span>
通道的接收端有两个有用的方法:`recv`和`try_recv`。这里,我们使用了`recv`,它是 *receive* 的缩写。这个方法会阻塞执行直到从通道中接收一个值。一旦发送了一个值,`recv`会在一个`Result<T, E>`中返回它。当通道发送端关闭,`recv`会返回一个错误。`try_recv`不会阻塞;相反它立刻返回一个`Result<T, E>`。 通道的接收端有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 *receive* 的缩写。这个方法会阻塞主线程执行直到从通道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result<T, E>` 中返回它。当通道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。
`try_recv` 不会阻塞,相反它立刻返回一个 `Result<T, E>``Ok` 值包含可用的信息,而 `Err` 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 `try_recv` 很有用:可以编写一个循环来频繁调用 `try_recv`,再有可用消息时进行处理,其余时候则处理一会其他工作知道再次检查。
处于简单的考虑,这个例子使用了 `recv`;主线程中除了等待消息之外没有任何其他工作,所以阻塞主线程是合适的。
如果运行示例 16-8 中的代码,我们将会看到主线程打印出这个值: 如果运行示例 16-8 中的代码,我们将会看到主线程打印出这个值:
``` ```text
Got: hi Got: hi
``` ```
### 通道与所有权如何交互 完美!
现在让我们做一个试验来看看通道与所有权如何在一起工作:我们将尝试在新建线程中的通道中发送完`val`之后再使用它。尝试编译示例 16-9 中的代码: ### 通道与所有权转移
所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。在并发编程中避免错误是在整个 Rust 程序中必须思考所有权所换来的一大优势。
现在让我们做一个试验来看看通道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的通道中发送完 `val`**之后** 再使用它。尝试编译示例 16-9 中的代码:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -117,11 +132,11 @@ fn main() {
<span class="caption">示例 16-9: 在我们已经发送到通道中后,尝试使用 `val` 引用</span> <span class="caption">示例 16-9: 在我们已经发送到通道中后,尝试使用 `val` 引用</span>
这里尝试在通过`tx.send`发送`val`到通道中之后将其打印出来。这是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们在此使用它之前就修改或者丢弃它。这会由于不一致或不存在的数据而导致错误或意外的结果。 这里尝试在通过 `tx.send` 发送 `val` 到通道中之后将其打印出来。允许么做是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。这会由于不一致或不存在的数据而导致错误或意外的结果。
尝试编译这些代码Rust 会报错 这是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。其他线程对值可能的修改会由于不一致或不存在的数据而导致错误或意外的结果。然而,尝试编译示例 16-9 的代码时Rust 会给出一个错误
``` ```text
error[E0382]: use of moved value: `val` error[E0382]: use of moved value: `val`
--> src/main.rs:10:31 --> src/main.rs:10:31
| |
@ -134,13 +149,11 @@ error[E0382]: use of moved value: `val`
not implement the `Copy` trait not implement the `Copy` trait
``` ```
我们的并发错误会造成一个编译时错误!`send`获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。 我们的并发错误会造成一个编译时错误。`send` 函数获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。
在这一点上,消息传递非常类似于 Rust 的单所有权系统。消息传递的拥护者出于相似的原因支持消息传递,就像 Rustacean 们欣赏 Rust 的所有权一样:单所有权意味着特定类型问题的消失。如果一次只有一个线程可以使用某些内存,就没有出现数据竞争的机会。
### 发送多个值并观察接收者的等待 ### 发送多个值并观察接收者的等待
示例 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。示例 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一段时间 示例 16-8 中的代码可以编译和运行,不过它并没有明前的告诉我们两个独立的线程通过通道相互通讯。示例 16-10 则有一些改进会证明示例 16-8 中的代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一秒钟
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -162,7 +175,7 @@ fn main() {
for val in vals { for val in vals {
tx.send(val).unwrap(); tx.send(val).unwrap();
thread::sleep(Duration::new(1, 0)); thread::sleep(Duration::from_secs(1));
} }
}); });
@ -176,35 +189,37 @@ fn main() {
这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。 这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。
在主线程中,不再显式调用`recv`函数:而是将`rx`当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。 在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。
当运行示例 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒: 当运行示例 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒:
``` ```text
Got: hi Got: hi
Got: from Got: from
Got: the Got: the
Got: thread Got: thread
``` ```
在主线程中并没有任何暂停或位于`for`循环中用于等待的代码,所以可以说主线程是在等待从新建线程中接收值。 因为在主线程中并没有任何暂停或位于 `for` 循环中用于等待的代码,所以可以说主线程是在等待从新建线程中接收值。
### 通过克隆发送者来创建多个生产者 ### 通过克隆发送者来创建多个生产者
差不多在本部分的开头,我们提到了`mpsc`是 *multiple producer, single consumer* 的缩写。可以扩展示例 16-11 中的代码来创建都向同一接收者发送值的多个线程。这可以通过克隆通道的发送端在来做到,如示例 16-11 所示: 之前我们提到了`mpsc`是 *multiple producer, single consumer* 的缩写。可以运用 `mpsc`扩展示例 16-11 中的代码来创建都向同一接收者发送值的多个线程。这可以通过克隆通道的发送端在来做到,如示例 16-11 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
```rust ```rust
# use std::thread; # use std::thread;
# use std::sync::mpsc; # use std::sync::mpsc;
# use std::time::Duration; # use std::time::Duration;
# #
# fn main() { # fn main() {
// ...snip... // --snip--
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let tx1 = tx.clone(); let tx1 = mpsc::Sender::clone(&tx);
thread::spawn(move || { thread::spawn(move || {
let vals = vec![ let vals = vec![
String::from("hi"), String::from("hi"),
@ -215,7 +230,7 @@ thread::spawn(move || {
for val in vals { for val in vals {
tx1.send(val).unwrap(); tx1.send(val).unwrap();
thread::sleep(Duration::new(1, 0)); thread::sleep(Duration::from_secs(1));
} }
}); });
@ -229,24 +244,25 @@ thread::spawn(move || {
for val in vals { for val in vals {
tx.send(val).unwrap(); tx.send(val).unwrap();
thread::sleep(Duration::new(1, 0)); thread::sleep(Duration::from_secs(1));
} }
}); });
// ...snip...
# for received in rx {
# for received in rx { println!("Got: {}", received);
# println!("Got: {}", received); }
# }
// --snip--
# } # }
``` ```
<span class="caption">示例 16-11: 发送多个消息,并在每次发送后暂停一段时间</span> <span class="caption">示例 16-11: 从多个生产者发送多个消息</span>
这一次,在创建新线程之前,我们对通道的发送端调用了`clone`方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程,这样每个线程将向通道的接收端发送不同的消息。 这一次,在创建新线程之前,我们对通道的发送端调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向通道的接收端发送不同的消息。
如果运行这些代码,你 **可能** 会看到这样的输出: 如果运行这些代码,你 **可能** 会看到这样的输出:
``` ```text
Got: hi Got: hi
Got: more Got: more
Got: from Got: from
@ -257,6 +273,6 @@ Got: thread
Got: you Got: you
``` ```
虽然你可能会看到这些以不同的顺序出现。这依赖于你的系统!这也就是并发既有趣又困难的原因。如果你拿`thread::sleep`做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定并每次都会产生不同的输出。 虽然你可能会看到这些值以不同的顺序出现;这依赖于你的系统。这也就是并发既有趣又困难的原因。如果通过 `thread::sleep` 做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定并每次都会产生不同的输出。
现在我们见识过了通道如何工作,再看看共享内存并发吧。 现在我们见识过了通道如何工作,再看看另一种不同的并发方式吧。

View File

@ -2,14 +2,15 @@
> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-03-shared-state.md) > [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-03-shared-state.md)
> <br> > <br>
> commit 9df612e93e038b05fc959db393c15a5402033f47 > commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7
虽然消息传递是一个很好的处理并发的方式,但并不是唯一的一个。再次考虑一下它的口号 虽然消息传递是一个很好的处理并发的方式,但并不是唯一一个。再一次思考一下 Go 编程语言文档中口号的这一部分:“通过共享内存通讯”
> Do not communicate by sharing memory; instead, share memory by > What would communicating by sharing memory look like? In addition, why would message passing enthusiasts not use it and do the opposite instead?
> communicating.
> >
> 不要共享内存来通讯;而是要通讯来共享内存。 > 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢?
在某种程度上
那么“共享内存来通讯”是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。 那么“共享内存来通讯”是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。