mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 00:43:59 +08:00
check to ch16-01
This commit is contained in:
parent
e9186fb9bb
commit
1b37e6c2f8
@ -537,7 +537,7 @@ error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
|
|||||||
|
|
||||||
当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。在示例 13-12 中,`equal_to_x` 闭包不可变的借用了 `x`(所以 `equal_to_x` 使用 `Fn` trait),因为闭包体只需要读取 `x` 的值。
|
当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。在示例 13-12 中,`equal_to_x` 闭包不可变的借用了 `x`(所以 `equal_to_x` 使用 `Fn` trait),因为闭包体只需要读取 `x` 的值。
|
||||||
|
|
||||||
如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这在将闭包传递给新线程以便将数据移动到新线程中时最为实用。
|
如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这个技巧在将闭包传递给新线程以便将数据移动到新线程中时最为实用。
|
||||||
|
|
||||||
第十六章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了示例 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动;注意这些代码还不能编译:
|
第十六章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了示例 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动;注意这些代码还不能编译:
|
||||||
|
|
||||||
|
@ -2,20 +2,19 @@
|
|||||||
|
|
||||||
> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-00-concurrency.md)
|
> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-00-concurrency.md)
|
||||||
> <br>
|
> <br>
|
||||||
> commit da15de39eaabd50100d6fa662c653169254d9175
|
> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7
|
||||||
|
|
||||||
确保内存安全并不是 Rust 的唯一目标:更好的处理并发和并行编程一直是 Rust 的另一个主要目标。
|
安全并高效的处理并发编程是 Rust 的另一个主要目标。**并发编程**(*Concurrent programming*),代表程序的不同部分相互独立的执行,而 **并行编程**(*parallel programming*)代表程序不同部分于同时执行,这两个概念随着计算机越来越多的利用多处理器的优势时显得愈发重要。由于历史原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一点。
|
||||||
**并发编程**(concurrent programming)代表程序的不同部分相互独立的执行,而**并行编程**代表程序不同部分同时执行,这两个概念在计算机拥有更多处理器可供程序利用时变得更加重要。由于历史的原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一点。
|
|
||||||
|
|
||||||
最开始,我们认为内存安全和防止并发问题是需要通过两个不同的方法解决的两个相互独立的挑战。然而,随着时间的推移,我们发现所有权和类型系统是一系列解决内存安全**和**并发问题的强用力的工具!通过改进所有权和类型检查,很多并发错误在 Rust 中都是**编译时**错误,而不是运行时错误。我们给 Rust 的这一部分起了一个绰号**无畏并发**(*fearless concurrency*)。无畏并发意味着 Rust 不光允许你自信代码不会出现诡异的错误,也让你可以轻易重构这种代码而无需担心会引入新的 bug。
|
起初,Rust 团队认为确保内存安全和防止并发问题是两个分别需要不同方法应对的挑战。随着时间的推移,团队发现所有权和类型系统是一系列解决内存安全 **和** 并发问题的强用力的工具!通过改进所有权和类型检查,Rust 很多并发错误都是 **编译时** 错误,而非运行时错误。因此,相比花费大量时间尝试重现运行时并发 bug 出现的特定情况,Rust 会拒绝编译不正确的代码并提供解释问题的错误信息。因此,你可以在开发时而不是不慎部署到生产环境后修复代码。我们给 Rust 的这一部分起了一个绰号 **无畏并发**(*fearless concurrency*)。无畏并发令你的代码免于出现诡异的 bug 并可以轻松重构且无需担心会引入新的 bug。
|
||||||
|
|
||||||
> 注意:对于 Rust 的口号**无畏并发**,这里用**并发**指代很多问题而不是更精确的区分**并发和(或)并行**,是出于简化问题的原因。如果这是一本专注于并发和/或并行的书,我们肯定会更精确的。对于本章,当我们谈到**并发**时,请自行替换为**并发和(或)并行**。
|
> 注意:出于简洁的考虑,我们将很多问题归为并发,而不是更准确的区分并发和(或)并行。如果这是一本专注于并发和/或并行的书,我们肯定会更加精确的。对于本章,当我们谈到并发时,请自行脑内替换为并发和(或)并行。
|
||||||
|
|
||||||
很多语言所提供的处理并发问题的解决方法都非常有特色,尤其是对于更高级的语言,这是一个非常合理的策略。然而对于底层语言则没有奢侈的选择。在任何给定的情况下,我们都期望底层语言可以提供最高的性能,并且对硬件有更薄的抽象。因此,Rust 给了我们多种工具,并以适合实际情况和需求的方式来为问题建模。
|
很多语言所提供的处理并发问题的解决方法都非常有特色。例如,Erlang 有着优雅的消息传递并发功能,但只有模糊不清的在线程间共享状态的方法。对于高级语言来说,只实现可能解决方案的子集是一个合理的策略,因为高级语言所许诺的价值来源于牺牲一些控制来换取抽象。然而对于底层语言则期望提供在任何给定的情况下有着最高的性能且对硬件有更少的抽象。因此,Rust 提供了多种工具,以符合实际情况和需求的方式来为问题建模。
|
||||||
|
|
||||||
如下是本章将要涉及到的内容:
|
如下是本章将要涉及到的内容:
|
||||||
|
|
||||||
* 如何创建线程来同时运行多段代码。
|
* 如何创建线程来同时运行多段代码。
|
||||||
* 并发**消息传递**(*Message passing*),其中通道(channel)被用来在线程间传递消息。
|
* **消息传递**(*Message passing*)并发,其中通道(channel)被用来在线程间传递消息。
|
||||||
* 并发**共享状态**(*Shared state*),其中多个线程可以访问同一片数据。
|
* **共享状态**(*Shared state*)并发,其中多个线程可以访问同一片数据。
|
||||||
* `Sync` 和 `Send` trait,他们允许 Rust 的并发保证能被扩展到用户定义的和标准库中提供的类型中。
|
* `Sync` 和 `Send` trait,他们允许 Rust 的并发保证能被扩展到用户定义的和标准库中提供的类型中。
|
@ -2,48 +2,58 @@
|
|||||||
|
|
||||||
> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-01-threads.md)
|
> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-01-threads.md)
|
||||||
> <br>
|
> <br>
|
||||||
> commit 55b294f20fc846a13a9be623bf322d8b364cee77
|
> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7
|
||||||
|
|
||||||
在今天使用的大部分操作系统中,当程序执行时,操作系统运行代码的上下文称为**进程**(*process*)。操作系统可以运行很多进程,而操作系统也管理这些进程使得多个程序可以在电脑上同时运行。
|
在大部分现代操作系统中,执行中程序的代码运行于一个 **进程**(*process*)中,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这个运行这些独立部分的功能被称为 **线程**(*threads*)。
|
||||||
|
|
||||||
我们可以将每个进程运行一个程序的概念再往下抽象一层:程序也可以在其上下文中同时运行独立的部分。这个功能叫做**线程**(*thread*)。
|
将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:
|
||||||
|
|
||||||
将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程,相比只期望在单个线程中运行的程序,仍然要采用不同的思考方式和代码结构。
|
* 竞争状态(Race conditions),多个线程以不一致的顺序访问数据或资源
|
||||||
|
* 死锁(Deadlocks),两个线程相互等待对方停止使用其所拥有的资源,这会阻止它们继续运行
|
||||||
|
* 只会发生在特定情况且难以稳定重现和修复的 bug
|
||||||
|
|
||||||
编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。另外,很多编程语言提供了自己的特殊的线程实现。编程语言提供的线程有时被称作**轻量级**(*lightweight*)或**绿色**(*green*)线程。这些语言将一系列绿色线程放入不同数量的操作系统线程中执行。因为这个原因,语言调用操作系统 API 创建线程的模型有时被称为 *1:1*,一个 OS 线程对应一个语言线程。绿色线程模型被称为 *M:N* 模型,`M`个绿色线程对应`N`个 OS 线程,这里`M`和`N`不必相同。
|
Rust 尝试缓和使用线程的负面影响。不过在多线程上下文中编程仍需格外小心,同时其所要求的代码结构也不同于运行于单线程的程序。
|
||||||
|
|
||||||
每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。**运行时**是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时为了保持高性能必需能够调用 C 语言,这点也是不能妥协的。
|
编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。这种由编程语言调用操作系统 API 创建线程的模模型有时被称为 *1:1*,一个 OS 线程对应一个语言线程。
|
||||||
|
|
||||||
绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更好的线程运行控制和更低的上下文切换成本。
|
很多编程语言提供了自己特殊的线程实现。编程语言提供的线程被称为 **绿色**(*green*)线程,使用绿色线程的语言会在不同数量的 OS 线程中执行它们。为此,绿色线程模式被称为 *M:N* 模型:`M` 个绿色线程对应 `N` 个 OS 线程,这里 `M` 和 `N` 不必相同。
|
||||||
|
|
||||||
|
每一个模型都有其优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。运行时是一个令人迷惑的概念,其在不同上下文中可能有不同的含义。
|
||||||
|
|
||||||
|
在当前上下文中,**运行时** 代表进制文件中包含的由语言自身提供的代码。这些代码根据语言的不同可大可小,不过任何非汇编语言都会有一定数量的运行时代码。为此,通常人们说一个语言 “没有运行时”,一般意味着 “小运行时”。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出,这使其易于在更多上下文中与其他语言向结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时为了保持高性能必需能够调用 C 语言,这点也是不能妥协的。
|
||||||
|
|
||||||
|
绿色线程的 M:N 模型更大的语言运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是如此底层的语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更好的线程运行控制和更低的上下文切换成本。
|
||||||
|
|
||||||
现在我们明白了 Rust 中的线程是如何定义的,让我们开始探索如何使用标准库提供的线程相关的 API 吧。
|
现在我们明白了 Rust 中的线程是如何定义的,让我们开始探索如何使用标准库提供的线程相关的 API 吧。
|
||||||
|
|
||||||
### 使用 `spawn` 创建新线程
|
### 使用 `spawn` 创建新线程
|
||||||
|
|
||||||
为了创建一个新线程,调用`thread::spawn`函数并传递一个闭包(第十三章学习了闭包),它包含希望在新线程运行的代码。示例 16-1 中的例子在新线程中打印了一些文本而其余的文本在主线程中打印:
|
为了创建一个新线程,需要调用 `thread::spawn` 函数并传递一个闭包(第十三章学习了闭包),其包含希望在新线程运行的代码。示例 16-1 中的例子在主线程打印了一些文本而另一些文本则由新线程打印:
|
||||||
|
|
||||||
<span class="filename">文件名: src/main.rs</span>
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
thread::spawn(|| {
|
thread::spawn(|| {
|
||||||
for i in 1..10 {
|
for i in 1..10 {
|
||||||
println!("hi number {} from the spawned thread!", i);
|
println!("hi number {} from the spawned thread!", i);
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for i in 1..5 {
|
for i in 1..5 {
|
||||||
println!("hi number {} from the main thread!", i);
|
println!("hi number {} from the main thread!", i);
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 16-1: 创建一个打印某些内容的新线程,但是主线程打印其它内容</span>
|
<span class="caption">示例 16-1: 创建一个打印某些内容的新线程,但是主线程打印其它内容</span>
|
||||||
|
|
||||||
|
注意这个函数编写的方式,当主线程结束时,新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:
|
||||||
注意这个函数编写的方式,当主线程结束时,它也会停止新线程。这个程序的输出每次可能都略微不同,不过它大体上看起来像这样:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
hi number 1 from the main thread!
|
hi number 1 from the main thread!
|
||||||
@ -57,38 +67,44 @@ hi number 4 from the spawned thread!
|
|||||||
hi number 5 from the spawned thread!
|
hi number 5 from the spawned thread!
|
||||||
```
|
```
|
||||||
|
|
||||||
这些线程可能会轮流运行,不过并不保证如此。在这里,主线程先行打印,即便新创建线程的打印语句位于程序的开头。甚至即便我们告诉新建的线程打印直到`i`等于 9 ,它在主线程结束之前也只打印到了 5。如果你只看到了一个线程,或没有出现重叠打印的现象,尝试增加 range 的数值来增加线程暂停并切换到其他线程运行的机会。
|
`thread::sleep` 调用强制线程停止执行一小段时间,这会允许其他不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如何调度线程。在这里,主线程首先打印,即便新创建线程的打印语句位于程序的开头。甚至即便我们告诉新建的线程打印直到 `i` 等于 9 ,它在主线程结束之前也只打印到了 5。
|
||||||
|
|
||||||
|
如果你只看到了主线程的输出,或没有出现重叠打印的现象,尝试增加 range 的数值来增加操作系统切换线程的机会。
|
||||||
|
|
||||||
#### 使用 `join` 等待所有线程结束
|
#### 使用 `join` 等待所有线程结束
|
||||||
|
|
||||||
由于主线程先于新建线程结束,不仅示例 16-1 中的代码大部分时候不能保证新建线程执行完毕,甚至不能实际保证新建线程会被执行!可以通过保存`thread::spawn`的返回值来解决这个问题,这是一个`JoinHandle`。这看起来如示例 16-2 所示:
|
由于主线程结束,示例 16-1 中的代码大部分时候不光会提早结束新建线程,甚至不能实际保证新建线程会被执行。其原因在于无法保证线程运行的顺序!
|
||||||
|
|
||||||
|
可以通过将 `thread::spawn` 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。`thread::spawn` 的返回值类型是 `JoinHandle`。`JoinHandle` 是一个拥有所有权的值,当对其调用 `join` 方法时,它会等待其线程结束。示例 16-2 展示了如何使用示例 16-1 这个中创建的线程的 `JoinHandle` 并调用 `join` 来确保新建线程在 `main` 退出前结束运行:
|
||||||
|
|
||||||
<span class="filename">文件名: src/main.rs</span>
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let handle = thread::spawn(|| {
|
let handle = thread::spawn(|| {
|
||||||
for i in 1..10 {
|
for i in 1..10 {
|
||||||
println!("hi number {} from the spawned thread!", i);
|
println!("hi number {} from the spawned thread!", i);
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for i in 1..5 {
|
for i in 1..5 {
|
||||||
println!("hi number {} from the main thread!", i);
|
println!("hi number {} from the main thread!", i);
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
handle.join();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle`,
|
<span class="caption">示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle` 以确保该线程能够运行至结束</span>
|
||||||
以确保该线程能够运行至结束</span>
|
|
||||||
|
|
||||||
`JoinHandle`是一个拥有所有权的值,它可以等待一个线程结束,这也正是`join`方法所做的。通过调用这个句柄的`join`,当前线程会阻塞直到句柄所代表的线程结束。因为我们将`join`调用放在了主线程的`for`循环之后,运行这个例子将产生类似这样的输出:
|
通过调用 handle 的 `join` 会阻塞当前线程直到 handle 所代表的线程结束。**阻塞**(*Blocking*) 线程意味着阻止该线程执行工作或退出。因为我们将 `join` 调用放在了主线程的 `for` 循环之后,运行示例 16-2 应该会产生类似这样的输出:
|
||||||
|
|
||||||
```
|
```text
|
||||||
hi number 1 from the main thread!
|
hi number 1 from the main thread!
|
||||||
hi number 2 from the main thread!
|
hi number 2 from the main thread!
|
||||||
hi number 1 from the spawned thread!
|
hi number 1 from the spawned thread!
|
||||||
@ -106,31 +122,34 @@ hi number 9 from the spawned thread!
|
|||||||
|
|
||||||
这两个线程仍然会交替执行,不过主线程会由于 `handle.join()` 调用会等待直到新建线程执行完毕。
|
这两个线程仍然会交替执行,不过主线程会由于 `handle.join()` 调用会等待直到新建线程执行完毕。
|
||||||
|
|
||||||
如果将`handle.join()`放在主线程的`for`循环之前,像这样:
|
不过让我们看看将 `handle.join()` 移动到 `main` 中 `for` 循环之前会发生什么,如下:
|
||||||
|
|
||||||
<span class="filename">文件名: src/main.rs</span>
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let handle = thread::spawn(|| {
|
let handle = thread::spawn(|| {
|
||||||
for i in 1..10 {
|
for i in 1..10 {
|
||||||
println!("hi number {} from the spawned thread!", i);
|
println!("hi number {} from the spawned thread!", i);
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
handle.join();
|
handle.join().unwrap();
|
||||||
|
|
||||||
for i in 1..5 {
|
for i in 1..5 {
|
||||||
println!("hi number {} from the main thread!", i);
|
println!("hi number {} from the main thread!", i);
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
主线程会等待直到新建线程执行完毕之后才开始执行`for`循环,所以输出将不会交替出现:
|
主线程会等待直到新建线程执行完毕之后才开始执行 `for` 循环,所以输出将不会交替出现,如下所示:
|
||||||
|
|
||||||
```
|
```text
|
||||||
hi number 1 from the spawned thread!
|
hi number 1 from the spawned thread!
|
||||||
hi number 2 from the spawned thread!
|
hi number 2 from the spawned thread!
|
||||||
hi number 3 from the spawned thread!
|
hi number 3 from the spawned thread!
|
||||||
@ -146,17 +165,17 @@ hi number 3 from the main thread!
|
|||||||
hi number 4 from the main thread!
|
hi number 4 from the main thread!
|
||||||
```
|
```
|
||||||
|
|
||||||
稍微考虑一下将`join`放置于何处会影响线程是否同时运行。
|
稍微考虑一下将 `join` 放置于何处这样一个细节会影响线程是否同时运行。
|
||||||
|
|
||||||
### 线程和`move`闭包
|
### 线程与 `move` 闭包
|
||||||
|
|
||||||
第十三章有一个我们没有讲到的闭包功能,它经常用于`thread::spawn`:`move`闭包。第十三章中讲到:
|
`move` 闭包,我们曾在第十三章简要的提到过,其经常与 `thread::spawn` 一起使用,因为它允许我们在一个线程中使用另一个线程的数据。
|
||||||
|
|
||||||
> 获取他们环境中值的闭包主要用于开始新线程的场景
|
第十三章讲到 “如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这个技巧在将闭包传递给新线程以便将数据移动到新线程中时最为实用。”
|
||||||
|
|
||||||
现在我们正在创建新线程,所以让我们讨论一下获取环境值的闭包吧!
|
现在我们正在创建新线程,所以让我们讨论一下在闭包中获取环境值吧。
|
||||||
|
|
||||||
注意示例 16-1 中传递给`thread::spawn`的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。示例 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作:
|
注意示例 16-1 中传递给 `thread::spawn` 的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。示例 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作,如下所示:
|
||||||
|
|
||||||
<span class="filename">文件名: src/main.rs</span>
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
@ -170,20 +189,18 @@ fn main() {
|
|||||||
println!("Here's a vector: {:?}", v);
|
println!("Here's a vector: {:?}", v);
|
||||||
});
|
});
|
||||||
|
|
||||||
handle.join();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 16-3: 在主线程中创建一个 vector,尝试在其它线程中使用它</span>
|
<span class="caption">示例 16-3: 尝试在另一个线程使用主线程创建的 vector</span>
|
||||||
|
|
||||||
闭包使用了`v`,所以闭包会获取`v`并使其成为闭包环境的一部分。因为`thread::spawn`在一个新线程中运行这个闭包,所以可以在新线程中访问`v`。
|
闭包使用了 `v`,所以闭包会捕获 `v` 并使其成为闭包环境的一部分。因为 `thread::spawn` 在一个新线程中运行这个闭包,所以可以在新线程中访问 `v`。然而当编译这个例子时,会得到如下错误:
|
||||||
|
|
||||||
然而当编译这个例子时,会得到如下错误:
|
```text
|
||||||
|
|
||||||
```
|
|
||||||
error[E0373]: closure may outlive the current function, but it borrows `v`,
|
error[E0373]: closure may outlive the current function, but it borrows `v`,
|
||||||
which is owned by the current function
|
which is owned by the current function
|
||||||
-->
|
--> src/main.rs:6:32
|
||||||
|
|
|
|
||||||
6 | let handle = thread::spawn(|| {
|
6 | let handle = thread::spawn(|| {
|
||||||
| ^^ may outlive borrowed value `v`
|
| ^^ may outlive borrowed value `v`
|
||||||
@ -191,13 +208,15 @@ which is owned by the current function
|
|||||||
| - `v` is borrowed here
|
| - `v` is borrowed here
|
||||||
|
|
|
|
||||||
help: to force the closure to take ownership of `v` (and any other referenced
|
help: to force the closure to take ownership of `v` (and any other referenced
|
||||||
variables), use the `move` keyword, as shown:
|
variables), use the `move` keyword
|
||||||
| let handle = thread::spawn(move || {
|
|
|
||||||
|
6 | let handle = thread::spawn(move || {
|
||||||
|
| ^^^^^^^
|
||||||
```
|
```
|
||||||
|
|
||||||
当在闭包环境中获取某些值时,Rust 会尝试推断如何获取它。`println!`只需要`v`的一个引用,所以闭包尝试借用`v`。但是这有一个问题:我们并不知道新建线程会运行多久,所以无法知道`v`是否一直时有效的。
|
Rust 会 **推断** 如何捕获 `v`,因为 `println!` 只需要 `v` 的引用,闭包尝试借用 `v`。然而这有一个问题:Rust 不知道这个新建线程会执行多久,所以无法知晓 `v` 的引用是否一直有效。
|
||||||
|
|
||||||
考虑一下示例 16-4 中的代码,它展示了一个`v`的引用很有可能不再有效的场景:
|
示例 16-4 展示了一个 `v` 的引用很有可能不再有效的场景:
|
||||||
|
|
||||||
<span class="filename">文件名: src/main.rs</span>
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
@ -213,23 +232,26 @@ fn main() {
|
|||||||
|
|
||||||
drop(v); // oh no!
|
drop(v); // oh no!
|
||||||
|
|
||||||
handle.join();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 16-4: 一个具有闭包的线程,尝试使用一个在主线程中被回收的引用 `v`</span>
|
<span class="caption">示例 16-4: 一个具有闭包的线程,尝试使用一个在主线程中被回收的引用 `v`</span>
|
||||||
|
|
||||||
这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个`v`的引用,不过主线程仍在执行:它立刻丢弃了`v`,使用了第十五章提到的显式丢弃其参数的`drop`函数。接着,新建线程开始执行,现在`v`是无效的了,所以它的引用也就是无效的。噢,这太糟了!
|
这段代码可以运行,而新建线程则可能会立刻被转移到后台并完全没有机会运行。新建线程内部有一个 `v` 的引用,不过主线程立刻就使用第十五章讨论的 `drop` 丢弃了 `v`。接着当新建线程开始执行,`v` 已不再有效,所以其引用也是无效的。噢,这太糟了!
|
||||||
|
|
||||||
为了修复这个问题,我们可以听取错误信息的建议:
|
|
||||||
|
|
||||||
```
|
为了修复示例 16-3 的编译错误,我们可以听取错误信息的建议:
|
||||||
|
|
||||||
|
```text
|
||||||
help: to force the closure to take ownership of `v` (and any other referenced
|
help: to force the closure to take ownership of `v` (and any other referenced
|
||||||
variables), use the `move` keyword, as shown:
|
variables), use the `move` keyword
|
||||||
| let handle = thread::spawn(move || {
|
|
|
||||||
|
6 | let handle = thread::spawn(move || {
|
||||||
|
| ^^^^^^^
|
||||||
```
|
```
|
||||||
|
|
||||||
通过在闭包之前增加`move`关键字,我们强制闭包获取它使用的值的所有权,而不是引用借用。示例 16-5 中展示的对示例 16-3 代码的修改可以按照我们的预期编译并运行:
|
通过在闭包之前增加 `move` 关键字,我们强制闭包获取其使用的值的所有权,而不是任由 Rust 推断它应该借用值。示例 16-5 中展示的对示例 16-3 代码的修改,这可以按照我们的预期编译并运行:
|
||||||
|
|
||||||
<span class="filename">文件名: src/main.rs</span>
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
@ -243,17 +265,17 @@ fn main() {
|
|||||||
println!("Here's a vector: {:?}", v);
|
println!("Here's a vector: {:?}", v);
|
||||||
});
|
});
|
||||||
|
|
||||||
handle.join();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 16-5: 使用 `move` 关键字强制获取它使用的值的所有权</span>
|
<span class="caption">示例 16-5: 使用 `move` 关键字强制获取它使用的值的所有权</span>
|
||||||
|
|
||||||
那么示例 16-4 中那个主线程调用了`drop`的代码该怎么办呢?如果在闭包上增加了`move`,就将`v`移动到了闭包的环境中,我们将不能对其调用`drop`了。相反会出现这个编译时错误:
|
那么如何使用了 `move` 闭包,示例 16-4 中主线程调用了 `drop` 的代码会发生什么呢?不幸的是,我们会因为示例 16-4 尝试进行由于不同的原因所不允许的操作而得到不同的错误。如果为闭包增加 `move`,将会把 `v` 移动进闭包的环境中,如此将不能在主线程中对其调用 `drop` 了。我们会得到如下不同的编译错误:
|
||||||
|
|
||||||
```
|
```text
|
||||||
error[E0382]: use of moved value: `v`
|
error[E0382]: use of moved value: `v`
|
||||||
-->
|
--> src/main.rs:10:10
|
||||||
|
|
|
|
||||||
6 | let handle = thread::spawn(move || {
|
6 | let handle = thread::spawn(move || {
|
||||||
| ------- value moved (into closure) here
|
| ------- value moved (into closure) here
|
||||||
@ -265,6 +287,6 @@ error[E0382]: use of moved value: `v`
|
|||||||
not implement the `Copy` trait
|
not implement the `Copy` trait
|
||||||
```
|
```
|
||||||
|
|
||||||
Rust 的所有权规则又一次帮助了我们!
|
Rust 的所有权规则又一次帮助了我们!示例 16-3 中的错误是因为 Rust 是保守的并只会为线程借用 `v`,这意味着主线程理论上可能使新建线程的引用无效。通过告诉 Rust 将 `v` 的所有权移动到新建线程,我们向 Rust 保证主线程不会再使用 `v`。如果对示例 16-4 也做出如此修改,那么当在主线程中使用 `v` 时就会违反所有权规则。 `move` 关键字覆盖了 Rust 默认保守的借用:其也不允许我们违反所有权规则。
|
||||||
|
|
||||||
现在我们有一个线程和线程 API 的基本了解,让我们讨论一下使用线程实际可以 **做** 什么吧。
|
现在我们有一个线程和线程 API 的基本了解,让我们讨论一下使用线程实际可以 **做** 什么吧。
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-02-message-passing.md)
|
> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-02-message-passing.md)
|
||||||
> <br>
|
> <br>
|
||||||
> commit da15de39eaabd50100d6fa662c653169254d9175
|
> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7
|
||||||
|
|
||||||
最近人气正在上升的一个并发方式是**消息传递**(*message passing*),这里线程或 actor 通过发送包含数据的消息来沟通。这个思想来源于口号:
|
最近人气正在上升的一个并发方式是**消息传递**(*message passing*),这里线程或 actor 通过发送包含数据的消息来沟通。这个思想来源于口号:
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user