Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions 1_Basic_of_Rust_Concurrency.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 第一章:Rust 并发基础

早在多核处理器司空见惯之前,操作系统就允许一台计算机运行多个程序。这是通过在进程之间快速切换来完成的,允许每个进程逐个地重重地取得一点进展。现在,几乎所有的电脑,甚至手机和手表都有着多核处理器,可以真正并行执行多个程序。
早在多核处理器司空见惯之前,操作系统就允许一台计算机运行多个程序。这是通过在进程之间快速切换来完成的,允许每个进程逐个地逐次取得一点进展。现在,几乎所有的电脑,甚至手机和手表都有着多核处理器,可以真正并行执行多个程序。

操作系统尽可能的将进程之间隔离,允许程序完全意识不到其他线程在做什么的情况下做自己的事情。例如,在不先询问操作系统内核的情况下,一个进程通常不能获取其他进程的内存,或者以任意方式与之通信。

Expand All @@ -14,7 +14,7 @@

每个程序都从一个线程开始:主(main)线程。该线程将执行你的 main 函数,并且如果需要,它可以用于产生更多线程。

在 Rust 中,新线程使用来自标准库的 `std::thread::spawn` 函数产生。它接受一个参数:新线程执行的函数。一旦该函数停止,将立刻返回
在 Rust 中,新线程使用来自标准库的 `std::thread::spawn` 函数产生。它接受一个参数:新线程执行的函数。一旦该函数返回,线程就会停止

让我们看一个示例:

Expand All @@ -40,7 +40,7 @@ fn f() {

<div class="box">
<h2 style="text-align: center;">Thread ID</h2>
Rust 标准库位每一个线程分配一个唯一的标识符。此标识符可以通过 Thread::id() 访问并且拥有 ThreadId 类型。除了复制 ThreadId 以及检查它们相等外,你也做不了什么。不能保证这些 ID 将会连续分配,只是每个线程都会有所不同。
Rust 标准库位每一个线程分配一个唯一的标识符。此标识符可以通过 Thread::id() 访问并且拥有 ThreadId 类型。除了复制 ThreadId 以及检查它们是否相等外,你也做不了什么。不能保证这些 ID 将会连续分配,只是每个线程都会有所不同。
</div>

如果你运行我们上面的示例几次,你可能注意到输出在运行之间有所不同。一次特定的运行在机器上的输出:
Expand All @@ -51,15 +51,15 @@ Hello from another thread!
This is my thread id:
```

惊讶地是,部分输出似乎失去了
惊讶的是,部分输出似乎丢失了

这里发生的情况是:新的线程结束执行它们的函数之前,主线程结束执行了主函数
这里发生的情况是:新的线程完成其函数的执行之前,主线程完成了主函数的执行

从主函数返回将退出整个程序,即使所有线程仍然在运行
从主函数返回将退出整个程序,即使其它线程仍然在运行

在这个特定的示例中,在程序被主线程关闭之前,其中一个新的线程有足够的消息到达第二条消息的一半。

如果我们想要在主函数返回之前,确保线程结束,我们可以通过 `join` 它们来等待。未来这样做,我们在 `spawn` 函数返回后使用 `JoinHandle`:
如果我们想要线程在主函数返回之前完成执行,我们可以通过 `join` 它们来等待。未来这样做,我们使用 `spawn` 函数返回的 `JoinHandle`:

```rust
fn main() {
Expand All @@ -75,7 +75,7 @@ fn main() {

`.join()` 方法等待直到线程结束执行并且返回 `std::thread::Result`。如果线程由于 panic 不能成功地完成它的函数,这将包含 panic 消息。我们试图去处理这种情况,或者为 join panic 的线程调用 `.unwrap()` 去 panic。

运行我们程序的这个版本,将不再导致截断的输出
运行我们程序的这个版本,将不再导致输出被截断

```txt
Hello from the main thread.
Expand All @@ -97,7 +97,7 @@ This is my thread id: ThreadId(3)

<div class="box">
<h2 style="text-align: center;">输出锁定</h2>
println 宏使用 <code>std::io::Stdout::lock()</code> 去确保输出没有被中断。<code>println!()</code> 将等待直到任意并发地运行完成后,在写入输出。如果不是这样,我们可以得到更多的交叉输出
println 宏使用 <code>std::io::Stdout::lock()</code> 去确保输出没有被中断。<code>println!()</code> 表达式将等待直到任意并发的表达式运行完成后,再写入输出。如果不是这样,我们可能得到更多的交错输出

<pre>
Hello fromHello from another thread!
Expand All @@ -107,7 +107,7 @@ This is my thread id: ThreadId(3)
id: ThreadId(3)</pre>
</div>

与其将函数的名称传递给 `std::thread::spawn`,不如像我们上面的示例那样,传递一个*闭包*。这允许我们捕获值移动到新的线程
与其将函数的名称传递给 `std::thread::spawn`(像我们上面的示例那样),不如传递一个*闭包*。这允许我们捕获值并移动到新的线程

```rust
let numbers = vec![1, 2, 3];
Expand All @@ -119,11 +119,11 @@ thread::spawn(move || {
}).join().unwrap();
```

在这里,numbers 的所有权被转移到新产生的线程,因为我们使用了 `move` 闭包。如果我们没有使用 `move` 关键字,闭包将会通过引用捕获 numbers。这将导致一个编译器错误,因为新的线程超出变量的生命周期
在这里,numbers 的所有权被转移到新产生的线程,因为我们使用了 `move` 闭包。如果我们没有使用 `move` 关键字,闭包将会通过引用捕获 numbers。这将导致一个编译器错误,因为新的线程比变量的生命周期更长

由于线程可能运行直到程序执行结束,因此产生的线程在它的参数类型上有 `'static` 生命周期绑定。换句话说,它只接受永久保留的函数。闭包通过引用捕获局部变量不能够永久保留,因为当局部变量不存在时,引用将变得无效。

从线程中取回一个值,是从闭包中返回完成的。该返回值通过 `join` 方法返回的 `Result` 中获取:
从线程中取回一个值,是从闭包中返回值来完成的。该返回值可以通过 `join` 方法返回的 `Result` 中获取:

```rust
let numbers = Vec::from_iter(0..=1000);
Expand All @@ -149,14 +149,14 @@ println!("average: {average}");

<p><code>std::thread::Builder</code> 允许你在产生线程之前为新线程设置一些设置。你可以使用它为新线程配置栈大小并给新线程一个名字。线程的名字是可以通过 <code>std::thread::current().name()</code> 获得,这将在 panic 消息中可用,并在监控和大多数雕饰工具中可见。</p>

<p>此外,Builder 的产生函数返回一个 <code>std::io::Result</code>,允许你处理新线程失败的情况。如果操作系统内存不足,或者资源限制已经应用于你对程序,这是可能发生的。如果 <code>std::thread::spawn</code> 函数不能去产生一个新线程,它只会 panic。</p>
<p>此外,Builder 的产生函数返回一个 <code>std::io::Result</code>,允许你处理产生新线程失败的情况。如果操作系统内存不足,或者资源限制已经应用于你的程序,这是可能发生的。如果 <code>std::thread::spawn</code> 函数不能去产生一个新线程,它就会 panic。</p>
</div>

## 线程作用域
## 作用域内的线程

如果我们确信生成的线程不会比某个范围存活更久,那么线程可以安全地借用哪些不会一直存在的东西,例如局部变量,只要它们比该范围活得更久。

Rust 标准库提供了 `std::thread::scope` 去产生此类*线程作用域*。它允许我们产生不超过我们传递给该函数闭包的范围的线程,这使它可能安全地借用局部变量。
Rust 标准库提供了 `std::thread::scope` 去产生此类*作用域内的线程*。它允许我们产生不超过我们传递给该函数闭包的范围的线程,这使它可能安全地借用局部变量。

它的工作原理最好使用一个示例来展示:

Expand Down Expand Up @@ -215,7 +215,7 @@ error[E0499]: cannot borrow `numbers` as mutable more than once at a time

<div class="box">
<h2 style="text-align: center;">泄漏启示录</h2>
<p>在 Rust 1.0 之前,标准库有一个函数叫做 <code>std::thread::scoped</code>,它将直接产生一个线程,就像 <code>std::thread::spawn</code>。它允许无 <code>'static</code> 的捕获,因为它返回的不是 JoinGuard,而是当被 drop 时 join 到线程的 JoinGuard。任意的借用数据仅需要比这个 JoinGuard 活得更久。只要 JoinGuard 在某个时候被 drop,这似乎是安全的。</p>
<p>在 Rust 1.0 之前,标准库有一个函数叫做 <code>std::thread::scoped</code>,它将直接产生一个线程,就像 <code>std::thread::spawn</code>。它允许无 <code>'static</code> 的捕获,因为它返回的不是 JoinHandle,而是当被 drop 时 join 到线程的 JoinGuard。任意的借用数据仅需要比这个 JoinGuard 活得更久。只要 JoinGuard 在某个时候被 drop,这似乎是安全的。</p>

<p>就在 Rust 1.0 发布之前,人们慢慢发现它似乎不能保证某些东西被 drop。有很多种方式没有 drop 它,例如创建一个引用计数节点的循环,可以忘记某些东西或者*泄漏*它。</p>

Expand All @@ -226,11 +226,11 @@ error[E0499]: cannot borrow `numbers` as mutable more than once at a time

## 共享所有权以及引用计数

目前,我们已经使用了 `move` 闭包([“Rust 中的线程”](#rust-中的线程))将值的所有权转移到线程并从生命周期较长的父线程借用数据([“线程作用域”](#线程作用域))。当两个线程之间共享数据,它们之间的任何一个线程都不能保证比另一个线程的生命周期长,那么它们都不能称为该数据的所有者。它们之间共享的任何数据都需要与最长生命周期的线程一样长。
目前,我们已经使用了 `move` 闭包([“Rust 中的线程”](#rust-中的线程))将值的所有权转移到线程并从生命周期较长的父线程借用数据([作用域内的线程](#作用域内的线程))。当两个线程之间共享数据,它们之间的任何一个线程都不能保证比另一个线程的生命周期长,那么它们都不能称为该数据的所有者。它们之间共享的任何数据都需要与最长生命周期的线程一样长。

### Static

有几种方式去创建不属于单线程的东西。最简单的方式是**静态**值,它由整个程序“拥有”,而不是单个线程。在以下示例中,这两个线程都可以获取 X,但是它们并不能拥有它
有几种方式去创建不属于单线程的东西。最简单的方式是**静态**值,它由整个程序“拥有”,而不是单个线程。在以下示例中,这两个线程都可以获取 X,但是它们并不拥有它

```rust
static X: [i32; 3] = [1, 2, 3];
Expand Down Expand Up @@ -262,7 +262,7 @@ thread::spawn(move || dbg!(x));

### 引用计数

为了确保共享数据能够 drop 和取消分配,我们不能完全放弃它的所有权。相反,我们可以*分享所有权*。通过跟踪所有者的数量,我们确保仅当没有所有者时,才会丢弃该值。
为了确保共享数据能够 drop 和释放内存,我们不能完全放弃它的所有权。相反,我们可以*分享所有权*。通过跟踪所有者的数量,我们确保仅当没有所有者时,才会丢弃该值。

Rust 标准库通过 `std::rc::Rc` 类型提供了该功能,它是“引用计数”(reference counted)的缩写。它与 `Box` 非常类似,唯一的区别是克隆它将不会分配任何新内存,而是增加存储在包含值旁边的计数器。原始的 `Rc` 和克隆的 `Rc` 将引用相同的内存分配;它们*共享所有权*。

Expand Down Expand Up @@ -397,7 +397,7 @@ let b = unsafe { a.get_unchecked(index) };</pre>

<p>如果我们破坏了这个假设,例如,我们以等于 3 的索引运行,任何事情都可能发生。它可能导致读取 a 之后存储的任何内存内容。这可能导致程序崩溃。它可能会执行程序中完全无关的部分。它可能会引起各种糟糕的情况。</p>

<p>或许令人惊讶的是,[未定义行为甚至可以“时间回溯”,导致之前的代码出问题](https://en.wikipedia.org/wiki/Speculative_execution)」。要理解这种情况是如何发生的,想象我们上面的片段有一个 match 语句,如下:</p>
<p>或许令人惊讶的是,<a href="https://en.wikipedia.org/wiki/Speculative_execution">未定义行为甚至可以“时间回溯”,导致之前的代码出问题</a>。要理解这种情况是如何发生的,想象我们上面的片段有一个 match 语句,如下:</p>

<pre>match index {
0 => x(),
Expand Down
2 changes: 1 addition & 1 deletion 2_Atomics.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ fn main() {
}
```

这次,我们使用一个[线程作用域](./1_Basic_of_Rust_Concurrency.md#线程作用域),它将自动地为我们处理线程的 join,并且也允许我们借用局部变量。
这次,我们使用一个[作用域内的线程](./1_Basic_of_Rust_Concurrency.md#作用域内的线程),它将自动地为我们处理线程的 join,并且也允许我们借用局部变量。

每次后台线程完成处理项时,它都会将处理的项目数量存储在 AtomicUsize 中。与此同时,主线程向用户显示该数字,告知该进度,大约每秒一次。一旦主线程看见所有 10 项已经被处理,它就会退出作用域,它会隐式地 join 后台线程,并且告知用户所有都完成。

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## [第一章:Rust 并发基础](./1_Basic_of_Rust_Concurrency.md)

* [Rust 中的线程](./1_Basic_of_Rust_Concurrency.md#rust-中的线程)
* [线程作用域](./1_Basic_of_Rust_Concurrency.md#线程作用域)
* [作用域内的线程](./1_Basic_of_Rust_Concurrency.md#作用域内的线程)
* [共享所有权以及引用计数](./1_Basic_of_Rust_Concurrency.md#共享所有权以及引用计数)
* [Static](./1_Basic_of_Rust_Concurrency.md#static)
* [泄漏(Leak)](./1_Basic_of_Rust_Concurrency.md#泄漏leak)
Expand Down