diff --git a/src/ch16-03-shared-state.md b/src/ch16-03-shared-state.md index 7ae0dbe..f83cdd4 100644 --- a/src/ch16-03-shared-state.md +++ b/src/ch16-03-shared-state.md @@ -10,26 +10,24 @@ > > 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢? -在某种程度上 - -那么“共享内存来通讯”是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。 - -不过 Rust 的类型系统和所有权可以很好的帮助我们,正确的管理它们。以共享内存中更常见的并发原语:互斥器(mutexes)为例,让我们看看具体的情况。 +在某种程度上,任何编程语言中的通道都类似于单所有权,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。 ### 互斥器一次只允许一个线程访问数据 -**互斥器**(*mutex*)是一种用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任意时间,它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住: +**互斥器**(*mutex*)是 “mutual exclusion” 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 **锁**(*lock*)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 **保护**(*guarding*)其数据。 + +互斥器以难以使用著称,因为你不得不记住: 1. 在使用数据之前尝试获取锁。 2. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。 -现实中也有互斥器的例子,想象一下在一个会议中,只有一个麦克风。如果一个成员要发言,他必须请求使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一个希望讲话的成员。如果成员在没有麦克风的时候就开始叫喊,或者在其他成员发言结束之前就拿走麦克风,是很不合适的。如果这个共享的麦克风因为此类原因而出现问题,会议将无法正常进行。 +作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法如期进行! 正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。 ### `Mutex`的 API -让我们看看示例 16-12 中使用互斥器的例子,现在不涉及多线程: +作为展示如何使用互斥器的例子,让我们从在单线程上下文使用互斥器开始,如示例 16-12 所示: 文件名: src/main.rs @@ -48,20 +46,22 @@ fn main() { } ``` -示例 16-12: 为简单,在一个单线程中探索 `Mutex` 的 API +示例 16-12: 出于简单的考虑,在一个单线程上下文中探索 `Mutex` 的 API -像很多类型一样,我们使用关联函数 `new` 来创建一个 `Mutex`。使用`lock`方法获取锁,以访问互斥器中的数据。这个调用会阻塞,直到我们拥有锁为止。如果另一个线程拥有锁,并且那个线程 panic 了,则这个调用会失败。类似于示例 16-6 那样,我们暂时使用 `unwrap()` 进行错误处理,或者使用第九章中提及的更好的工具。 +像很多类型一样,我们使用关联函数 `new` 来创建一个 `Mutex`。使用 `lock` 方法获取锁,以访问互斥器中的数据。这个调用会阻塞当前线程,直到我们拥有锁为止。 + +如果另一个线程拥有锁,并且那个线程 panic 了,则 `lock` 调用会失败。在这种情况下,没人能够再获取锁,所以这里选择 `unwrap` 并在遇到这种情况时使线程 panic。 -一旦获取了锁,就可以将返回值(在这里是`num`)作为一个数据的可变引用使用了。观察 Rust 类型系统如何保证使用值之前必须获取锁:`Mutex`并不是一个`i32`,所以**必须**获取锁才能使用这个`i32`值。我们是不会忘记这么做的,因为类型系统不允许。 +一旦获取了锁,就可以将返回值(在这里是`num`)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁:`Mutex` 并不是一个 `i32`,所以 **必须** 获取锁才能使用这个 `i32` 值。我们是不会忘记这么做的,因为反之类型系统不允许访问内部的 `i32` 值。 +正如你所怀疑的,`Mutex` 是一个智能指针。更准确的说,`lock` 调用 **返回** 一个叫做 `MutexGuard` 的智能指针。这个智能指针实现了 `Deref` 来指向其内部数据;其也提供了一个 `Drop` 实现当 `MutexGuard` 离开作用域时自动释放锁,这正发生于示例 16-12 内部作用域的结尾。为此,我们不会冒忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。 -你也许会怀疑,`Mutex`是一个智能指针?是的!更准确的说,`lock`调用返回一个叫做`MutexGuard`的智能指针。类似我们在第十五章见过的智能指针,它实现了`Deref`来指向其内部数据。另外`MutexGuard`有一个用来释放锁的`Drop`实现。这样就不会忘记释放锁了。这在`MutexGuard`离开作用域时会自动发生,例如它发生于示例 16-12 中内部作用域的结尾。接着可以打印出互斥器的值并发现能够将其内部的`i32`改为 6。 +丢弃了锁之后,可以打印出互斥器的值,并发现能够将其内部的 `i32` 改为 6。 -#### 在线程间共享`Mutex` +#### 在线程间共享 `Mutex` -现在让我们尝试使用`Mutex`在多个线程间共享值。我们将启动十个线程,并在各个线程中对同一个计数器值加一,这样计数器将从 0 变为 10。注意,接下来的几个例子会出现编译错误,而我们将通过这些错误来学习如何使用 -`Mutex`,以及 Rust 又是如何辅助我们以确保正确。示例 16-13 是最开始的例子: +现在让我们尝试使用 `Mutex` 在多个线程间共享值。我们将启动十个线程,并在各个线程中对同一个计数器值加一,这样计数器将从 0 变为 10。注意,接下来的几个例子会出现编译错误,而我们将通过这些错误来学习如何使用 `Mutex`,以及 Rust 又是如何帮助我们正确正确使用的。示例 16-13 是最开始的例子: 文件名: src/main.rs @@ -74,7 +74,7 @@ fn main() { let mut handles = vec![]; for _ in 0..10 { - let handle = thread::spawn(|| { + let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; @@ -92,38 +92,16 @@ fn main() { 示例 16-13: 程序启动了 10 个线程,每个线程都通过 `Mutex` 来增加计数器的值 -这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex`,类似示例 16-12 那样。接下来使用 range 创建了 10 个线程。使用了 `thread::spawn` 并对所有线程使用了相同的闭包:他们每一个都将调用 `lock` 方法来获取 `Mutex` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。 +这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex`,类似示例 16-12 那样。接下来遍历 range 创建了 10 个线程。使用了 `thread::spawn` 并对所有线程使用了相同的闭包:他们每一个都将调用 `lock` 方法来获取 `Mutex` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。 在主线程中,我们像示例 16-2 那样收集了所有的 join 句柄,调用它们的 `join` 方法来确保所有线程都会结束。之后,主线程会获取锁并打印出程序的结果。 之前提示过这个例子不能编译,让我们看看为什么! -``` -error[E0373]: closure may outlive the current function, but it borrows -`counter`, which is owned by the current function - --> - | -9 | let handle = thread::spawn(|| { - | ^^ may outlive borrowed value `counter` -10 | let mut num = counter.lock().unwrap(); - | ------- `counter` is borrowed here - | -help: to force the closure to take ownership of `counter` (and any other -referenced variables), use the `move` keyword, as shown: - | let handle = thread::spawn(move || { -``` -这类似于示例 16-5 中解决了的问题。考虑到启动了多个线程,Rust 无法知道这些线程会运行多久,而在每一个线程尝试借用 `counter` 时它是否仍然有效。帮助信息提醒了我们如何解决它:可以使用 `move` 来给予每个线程其所有权。尝试在闭包上做一点改动: - -```rust,ignore -thread::spawn(move || { -``` - -再次编译。这回出现了一个不同的错误! - -``` +```text error[E0382]: capture of moved value: `counter` - --> + --> src/main.rs:10:27 | 9 | let handle = thread::spawn(move || { | ------- value moved (into closure) here @@ -134,7 +112,7 @@ error[E0382]: capture of moved value: `counter` which does not implement the `Copy` trait error[E0382]: use of moved value: `counter` - --> + --> src/main.rs:21:29 | 9 | let handle = thread::spawn(move || { | ------- value moved (into closure) here @@ -148,9 +126,9 @@ error[E0382]: use of moved value: `counter` error: aborting due to 2 previous errors ``` -`move` 并没有像示例 16-5 中那样解决问题。为什么呢?错误信息有点难懂,因为它表明 `counter` 被移动进了闭包,接着它在调用 `lock` 时被捕获。这似乎是我们希望的,然而不被允许。 +错误信息表明 `counter` 值被移动进了闭包并当调用 `lock` 时被捕获。这听起来正是我们需要的,但是这是不允许的! -让我们推理一下。这次不再使用 `for` 循环创建 10 个线程,只创建两个线程,看看会发生什么。将示例 16-13 中第一个`for`循环替换为如下代码: +让我们简化程序来进行分析。不同于在 `for` 循环中创建 10 个线程,仅仅创建两个线程来观察发生了什么。将示例 16-13 中第一个 `for` 循环替换为如下代码: ```rust,ignore let handle = thread::spawn(move || { @@ -168,23 +146,23 @@ let handle2 = thread::spawn(move || { handles.push(handle2); ``` -这里创建了两个线程,并将第二个线程所用的变量改名为 `handle2` 和 `num2`。我们简化了例子,看是否能理解错误信息。此次编译给出如下信息: +这里创建了两个线程并将用于第二个线程的变量名改为 `handle2` 和 `num2`。这一次当运行代码时,编译会给出如下错误: ```text error[E0382]: capture of moved value: `counter` - --> + --> src/main.rs:16:24 | 8 | let handle = thread::spawn(move || { | ------- value moved (into closure) here ... -16 | let mut num = counter.lock().unwrap(); - | ^^^^^^^ value captured here after move +16 | let mut num2 = counter.lock().unwrap(); + | ^^^^^^^ value captured here after move | = note: move occurs because `counter` has type `std::sync::Mutex`, which does not implement the `Copy` trait error[E0382]: use of moved value: `counter` - --> + --> src/main.rs:26:29 | 8 | let handle = thread::spawn(move || { | ------- value moved (into closure) here @@ -198,15 +176,15 @@ error[E0382]: use of moved value: `counter` error: aborting due to 2 previous errors ``` -啊哈!第一个错误信息中说,`counter` 被移动进了 `handle` 所代表线程的闭包中。因此我们无法在第二个线程中对其调用 `lock`,并将结果储存在 `num2` 中时捕获`counter`!所以 Rust 告诉我们不能将 `counter` 的所有权移动到多个线程中。这在之前很难看出,因为我们在循环中创建了多个线程,而 Rust 无法在每次迭代中指明不同的线程(没有临时变量 `num2`)。 +啊哈!第一个错误信息中说,`counter` 被移动进了 `handle` 所代表线程的闭包中。因此我们无法在第二个线程中对其调用 `lock`,并将结果储存在 `num2` 中时捕获`counter`!所以 Rust 告诉我们不能将 `counter` 的所有权移动到多个线程中。这在之前很难看出,因为我们在循环中创建了多个线程,而 Rust 无法在每次迭代中指明不同的线程。让我们通过一个第十五章讨论过的多所有权手段来修复这个编译错误。 #### 多线程和多所有权 -在第十五章中,我们通过使用智能指针 `Rc` 来创建引用计数的值,以便拥有多所有权。同时第十五章提到了 `Rc` 只能在单线程环境中使用,不过还是在这里试用 `Rc` 看看会发生什么。示例 16-14 将 `Mutex` 装进了 `Rc` 中,并在移入线程之前克隆了 `Rc`。再用循环来创建线程,保留闭包中的 `move` 关键字: +在第十五章中,通过使用智能指针 `Rc` 来创建引用计数的值,以便拥有多所有者。让我们在这也这么做看看会发生什么。将示例 16-14 中的 `Mutex` 封装进 `Rc` 中并在将所有权移入线程之前克隆了 `Rc`。现在我们理解了所发生的错误,同时也将代码改回使用 `for` 循环,并保留闭包的 `move` 关键字: 文件名: src/main.rs -```rust +```rust,ignore use std::rc::Rc; use std::sync::Mutex; use std::thread; @@ -216,7 +194,7 @@ fn main() { let mut handles = vec![]; for _ in 0..10 { - let counter = counter.clone(); + let counter = Rc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); @@ -237,34 +215,37 @@ fn main() { 再一次编译并...出现了不同的错误!编译器真是教会了我们很多! -``` +```text error[E0277]: the trait bound `std::rc::Rc>: -std::marker::Send` is not satisfied - --> +std::marker::Send` is not satisfied in `[closure@src/main.rs:11:36: +15:10 +counter:std::rc::Rc>]` + --> src/main.rs:11:22 | 11 | let handle = thread::spawn(move || { - | ^^^^^^^^^^^^^ the trait `std::marker::Send` is not - implemented for `std::rc::Rc>` + | ^^^^^^^^^^^^^ `std::rc::Rc>` +cannot be sent between threads safely | - = note: `std::rc::Rc>` cannot be sent between threads - safely + = help: within `[closure@src/main.rs:11:36: 15:10 +counter:std::rc::Rc>]`, the trait `std::marker::Send` is +not implemented for `std::rc::Rc>` = note: required because it appears within the type - `[closure@src/main.rs:11:36: 15:10 - counter:std::rc::Rc>]` +`[closure@src/main.rs:11:36: 15:10 +counter:std::rc::Rc>]` = note: required by `std::thread::spawn` ``` -哇哦,太长不看!说重点:第一个提示表明 `Rc>` 不能安全的在线程间传递。理由也在错误信息中,“不满足 `Send` trait bound”(`the trait bound Send is not satisfied`)。下一部分将会讨论 `Send`,它是确保许多用在多线程中的类型,能够适合并发环境的 trait 之一。 +哇哦,错误信息太长不看!这里是一些需要注意的重要部分:第一行错误表明 `` `std::rc::Rc>` cannot be sent between threads safely ``。其原因是另一个值得注意的部分,经过提炼的错误信息表明 `` the trait bound `Send` is not satisfied ``。下一部分会讲到 `Send`:这是确保所使用的类型意在用于并发环境的 trait 之一。 -不幸的是,`Rc` 并不能安全的在线程间共享。当 `Rc` 管理引用计数时,它必须在每一个 `clone` 调用时增加计数,并在每一个克隆被丢弃时减少计数。`Rc` 并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。如果有一个类型与 `Rc` 相似,又以一种线程安全的方式改变引用计数,会怎么样呢? +不幸的是,`Rc` 并不能安全的在线程间共享。当 `Rc` 管理引用计数时,它必须在每一个 `clone` 调用时增加计数,并在每一个克隆被丢弃时减少计数。`Rc` 并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。我们所需要的是一个完全类似 `Rc`,又以一种线程安全的方式改变引用计数的类型。 #### 原子引用计数 `Arc` -答案是肯定的,确实有一个类似`Rc`并可以安全的用于并发环境的类型:`Arc`。字母“a”代表**原子性**(*atomic*),所以这是一个**原子引用计数**(*atomically reference counted*)类型。原子性是另一类这里还未涉及到的并发原语;请查看标准库中`std::sync::atomic`的文档来获取更多细节。其中的要点就是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。 +所幸 `Arc` **正是** 这么一个类似 `Rc` 并可以安全的用于并发环境的类型。字母 “a” 代表 **原子性**(*atomic*),所以这是一个**原子引用计数**(*atomically reference counted*)类型。原子性是另一类这里还未涉及到的并发原语:请查看标准库中 `std::sync::atomic` 的文档来获取更多细节。其中的要点就是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。 -为什么不是所有的原始类型都是原子性的?为什么不是所有标准库中的类型都默认使用`Arc`实现?线程安全带来性能惩罚,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,代码可以因此运行的更快。 +你可能会好奇为什么不是所有的原始类型都是原子性的?为什么不是所有标准库中的类型都默认使用 `Arc` 实现?原因在于线程安全带有性能惩罚,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,代码可以因此运行的更快。 -回到之前的例子:`Arc`和`Rc`除了`Arc`内部的原子性之外没有区别。其 API 也相同,所以可以修改`use`行和`new`调用。示例 16-15 中的代码最终可以编译和运行: +回到之前的例子:`Arc` 和 `Rc` 有着相同的 API,所以修改程序修改 `use` 行和 `new` 调用。示例 16-15 中的代码最终可以编译和运行: 文件名: src/main.rs @@ -277,7 +258,7 @@ fn main() { let mut handles = vec![]; for _ in 0..10 { - let counter = counter.clone(); + let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); @@ -298,16 +279,16 @@ fn main() { 这会打印出: -``` +```text Result: 10 ``` -成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们学习了很多关于`Mutex`和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。能够被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用`Mutex`来允许每个线程在他们自己的部分更新最终的结果。 +成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们确实学习了很多关于 `Mutex` 和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,可将计算分成独立的部分,分散到多个线程中,接着使用 `Mutex` 使用各自的结算结果更新最终的结果。 -你可能注意到了,因为`counter`是不可变的,不过可以获取其内部值的可变引用,这意味着`Mutex`提供了内部可变性,就像`Cell`系列类型那样。正如第十五章中使用`RefCell`可以改变`Rc`中的内容那样,同样的可以使用`Mutex`来改变`Arc`中的内容。 +### `RefCell`/`Rc` 与 `Mutex`/`Arc` 的相似性 -回忆一下`Rc`并没有避免所有可能的问题:我们也讨论了当两个`Rc`相互引用时的引用循环的可能性,这可能造成内存泄露。`Mutex`有一个类似的 Rust 同样也不能避免的问题:死锁。**死锁**(*deadlock*)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中`Mutex`和`MutexGuard`的 API 文档会提供有用的信息。 +你可能注意到了,因为 `counter` 是不可变的,不过可以获取其内部值的可变引用;这意味着 `Mutex` 提供了内部可变性,就像 `Cell` 系列类型那样。正如第十五章中使用 `RefCell` 可以改变 `Rc` 中的内容那样,同样的可以使用 `Mutex` 来改变 `Arc` 中的内容。 -Rust 的类型系统和所有权规则,确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以不可预测的方式覆盖彼此的操作。虽然为了使一切正确运行而在编译器上花了一些时间,但是我们节省了未来的时间,尤其是线程以特定顺序执行才会出现的诡异错误难以重现。 +另一个值得注意的细节是 Rust 不能避免使用 `Mutex` 的全部逻辑错误。回忆一下第十五章使用 `Rc` 就有造成引用循环的风险,这时两个 `Rc` 值相互引用,造成内存泄露。同理,`Mutex` 也有造成 **死锁**(*deadlock*) 的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中 `Mutex` 和 `MutexGuard` 的 API 文档会提供有用的信息。 -接下来,为了丰富本章的内容,让我们讨论一下`Send`和`Sync` trait 以及如何对自定义类型使用他们。 \ No newline at end of file +接下来,为了丰富本章的内容,让我们讨论一下 `Send`和 `Sync` trait 以及如何对自定义类型使用他们。 \ No newline at end of file diff --git a/src/ch16-04-extensible-concurrency-sync-and-send.md b/src/ch16-04-extensible-concurrency-sync-and-send.md index c72d4f7..a03773d 100644 --- a/src/ch16-04-extensible-concurrency-sync-and-send.md +++ b/src/ch16-04-extensible-concurrency-sync-and-send.md @@ -1,41 +1,41 @@ -## 使用`Sync`和`Send` trait 的可扩展并发 +## 使用 `Sync` 和 `Send` trait 的可扩展并发 > [ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-04-extensible-concurrency-sync-and-send.md) >
-> commit 9430a3d28a2121a938d704ce48b15d21062f880e +> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7 -Rust 的并发模型中一个有趣的方面是:语言本身对并发知之**甚少**。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的。 +Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 **甚少**。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。 -我们说“**几乎**所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于`std::marker`: `Sync`和`Send`。 +然而有两个并发概念是内嵌于语言中的:`std::marker` 中的 `Sync` 和 `Send` trait。 -### `Send`用于表明所有权可能被传送给其他线程 +### 通过 `Send` 允许在线程间转移所有权 -`Send`标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是`Send`的,不过有一些例外。比如标准库中提供的 `Rc`:如果克隆`Rc`值,并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,`Rc`被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。 +`Send` 标记 trait 表明类型的所有权可以在线程间传递。几乎所有的 Rust 类型都是`Send` 的,不过有一些例外,包括 `Rc`:这是不能 `Send` 的,因为如果克隆了 `Rc` 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,`Rc` 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。 -因为 `Rc` 没有标记为 `Send`,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个 `Rc` 值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,`the trait Send is not implemented for Rc>`。而使用标记为 `Send` 的 `Arc` 时,就没有问题了。 +因此,Rust 类型系统和 trait bound 确保永远也不会意外的将不安全的 `Rc` 在线程间发送。当尝试在示例 16-14 中这么做的时候,会得到错误 `the trait Send is not implemented for Rc>`。而使用标记为 `Send` 的 `Arc` 时,就没有问题了。 -任何完全由 `Send` 的类型组成的类型也会自动被标记为 `Send`:几乎所有基本类型都是 `Send` 的,大部分标准库类型是`Send`的,除了`Rc`,以及第十九章将会讨论的裸指针(raw pointer)。 +任何完全由 `Send` 的类型组成的类型也会自动被标记为 `Send`。几乎所有基本类型都是 `Send` 的,除了第十九章将会讨论的裸指针(raw pointer)。 -### `Sync` 表明多线程访问是安全的 +### `Sync` 允许多线程访问 -`Sync` 标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 `T`,如果`&T`(`T`的引用)是`Send`的话`T`就是`Sync`的,这样其引用就可以安全的发送到另一个线程。类似于 `Send` 的情况,基本类型是 `Sync` 的,完全由 `Sync` 的类型组成的类型也是 `Sync` 的。 +`Sync` 标记 trait 表明一个实现了 `Sync` 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 `T`,如果 `&T`(`T` 的引用)是 `Send` 的话 `T` 就是 `Sync` 的,这意味着其引用就可以安全的发送到另一个线程。类似于 `Send` 的情况,基本类型是 `Sync` 的,完全由 `Sync` 的类型组成的类型也是 `Sync` 的。 -`Rc` 也不是 `Sync` 的,出于其不是`Send`的相同的原因。`RefCell`(第十五章讨论过)和`Cell`系列类型不是`Sync`的。`RefCell`在运行时所进行的借用检查也不是线程安全的。`Mutex`是`Sync`的,正如上一部分所讲的它可以被用来在多线程中共享访问。 +智能指针 `Rc` 也不是 `Sync` 的,出于其不是 `Send` 相同的原因。`RefCell`(第十五章讨论过)和 `Cell` 系列类型不是 `Sync` 的。`RefCell` 在运行时所进行的借用检查也不是线程安全的。`Mutex` 是 `Sync` 的,正如 “在线程间共享 `Mutex`” 部分所讲的它可以被用来在多线程中共享访问。 -### 手动实现`Send`和`Sync`是不安全的 +### 手动实现 `Send` 和 `Sync` 是不安全的 -通常并不需要实现`Send`和`Sync` trait,由属于`Send`和`Sync`的类型组成的类型,自动就是`Send`和`Sync`的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。 +通常并不需要手动实现 `Send` 和 `Sync` trait,因为由 `Send` 和 `Sync` 的类型组成的类型,自动就是 `Send` 和 `Sync` 的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。 -实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是`Send`和`Sync`的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[The Nomicon] 中有更多关于这些保证以及如何维持他们的信息。 +手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是 `Send` 和 `Sync` 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[The Nomicon] 中有更多关于这些保证以及如何维持他们的信息。 [The Nomicon]: https://doc.rust-lang.org/stable/nomicon/ ## 总结 -这不会是本书最后一个出现并发的章节;第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。 +这不会是本书最后一个出现并发的章节:第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。 -正如我们提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。 +正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。 -Rust 提供了用于消息传递的通道,和像`Mutex`和`Arc`这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧! +Rust 提供了用于消息传递的通道,和像 `Mutex` 和 `Arc` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧! 接下来,让我们讨论一下当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系的。 \ No newline at end of file diff --git a/src/ch17-00-oop.md b/src/ch17-00-oop.md index 355b887..68c6c99 100644 --- a/src/ch17-00-oop.md +++ b/src/ch17-00-oop.md @@ -2,6 +2,6 @@ > [ch17-00-oop.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-00-oop.md) >
-> commit 759801361bde74b47e81755fff545c66020e6e63 +> commit 28d0efb644d18e8d104c2e813c8cdce50d040d3d -面向对象编程(Object-Oriented Programming)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。关于 OOP 是什么有很多相互矛盾的定义:在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。 \ No newline at end of file +面向对象编程(Object-Oriented Programming)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。关于 OOP 是什么有很多相互矛盾的定义,在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。 \ No newline at end of file