From 941eed2f2466ec78dd367ffd1c2a1fafc026c147 Mon Sep 17 00:00:00 2001 From: kazeno Date: Mon, 26 May 2025 16:54:25 +0800 Subject: [PATCH] wip: 2024 edition --- src/SUMMARY.md | 2 +- src/ch16-02-message-passing.md | 45 ++++++++--------- src/ch16-03-shared-state.md | 50 +++++++++---------- ...04-extensible-concurrency-sync-and-send.md | 17 +++---- src/ch17-02-concurrency-with-async.md | 2 +- src/ch18-00-oop.md | 9 ++-- 6 files changed, 61 insertions(+), 64 deletions(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 5793201..cc3616f 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -109,7 +109,7 @@ - [深入理解 async 相关的 traits](ch17-05-traits-for-async.md) - [future、任务和线程](ch17-06-futures-tasks-threads.md) -- [Rust 的面向对象编程特性](ch18-00-oop.md) +- [面向对象编程特性](ch18-00-oop.md) - [面向对象语言的特点](ch18-01-what-is-oo.md) - [顾及不同类型值的 trait 对象](ch18-02-trait-objects.md) - [面向对象设计模式的实现](ch18-03-oo-design-patterns.md) diff --git a/src/ch16-02-message-passing.md b/src/ch16-02-message-passing.md index 86aede2..f4b62dc 100644 --- a/src/ch16-02-message-passing.md +++ b/src/ch16-02-message-passing.md @@ -1,16 +1,15 @@ ## 使用消息传递在线程间传送数据 -> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/main/src/ch16-02-message-passing.md) ->
-> commit 36383b4da21dbd0a0781473bc8ad7ef0ed1b6751 + + -一个日益流行的确保安全并发的方式是 **消息传递**(_message passing_),这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 [Go 编程语言文档中](https://golang.org/doc/effective_go.html#concurrency) 的口号:“不要通过共享内存来通讯;而是通过通讯来共享内存。”(“Do not communicate by sharing memory; instead, share memory by communicating.”) +一个日益流行的确保安全并发的方式是**消息传递**(_message passing_),这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 [Go 编程语言文档中](https://golang.org/doc/effective_go.html#concurrency) 的口号:“不要通过共享内存来通讯;而要通过通讯来共享内存。”(“Do not communicate by sharing memory; instead, share memory by communicating.”) -为了实现消息传递并发,Rust 标准库提供了一个 **信道**(_channel_)实现。信道是一个通用编程概念,表示数据从一个线程发送到另一个线程。 +为了实现消息传递并发,Rust 标准库提供了一个**信道**(_channel_)实现。信道是一个通用编程概念,表示数据从一个线程发送到另一个线程。 -你可以将编程中的信道想象为一个水流的渠道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游。 +你可以将编程中的信道想象为一个水流的渠道,比如河流或小溪。如果你将诸如橡皮鸭之类的东西放入其中,它们会顺流而下到达下游。 -编程中的信息渠道(信道)有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。发送者位于上游位置,在这里可以将橡皮鸭放入河中,接收者则位于下游,橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据,另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为信道被 **关闭**(_closed_)了。 +信道由两个组成部分:一个发送端(transmitter)和一个接收端(receiver)。发送端位于上游位置,在这里可以将橡皮鸭放入河中,接收端则位于下游,橡皮鸭最终会漂流至此。代码中的一部分调用发送端的方法以及希望发送的数据,另一部分则检查接收端收到的消息。当发送端或接收端任一被丢弃时可以认为信道被**关闭**(_closed_)了。 这里,我们将开发一个程序,它会在一个线程生成值向信道发送,而在另一个线程会接收值并打印出来。这里会通过信道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,你就可以将信道用于任何相互通信的任何线程,例如一个聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。 @@ -24,9 +23,9 @@ 示例 16-6: 创建一个信道,并将其两端赋值给 `tx` 和 `rx` -这里使用 `mpsc::channel` 函数创建一个新的信道;`mpsc` 是 **多个生产者,单个消费者**(_multiple producer, single consumer_)的缩写。简而言之,Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 **发送**(_sending_)端,但只能有一个消费这些值的 **接收**(_receiving_)端。想象一下多条小河小溪最终汇聚成大河:所有通过这些小河发出的东西最后都会来到下游的大河。目前我们以单个生产者开始,但是当示例可以工作后会增加多个生产者。 +这里使用 `mpsc::channel` 函数创建一个新的信道;`mpsc` 是 **多生产者,单消费者**(_multiple producer, single consumer_)的缩写。简而言之,Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 **发送端**(_sending_),但只能有一个消费这些值的**接收端**(_receiving_)。想象一下多条小河小溪最终汇聚成大河:所有通过这些小河发出的东西最后都会来到下游的大河。目前我们以单个生产者开始,但是当示例可以工作后会增加多个生产者。 -`mpsc::channel` 函数返回一个元组:第一个元素是发送端 -- 发送者,而第二个元素是接收端 -- 接收者。由于历史原因,`tx` 和 `rx` 通常作为 **发送者**(_transmitter_)和 **接收者**(_receiver_)的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十九章会讨论 `let` 语句中的模式和解构。现在只需知道使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。 +`mpsc::channel` 函数返回一个元组:第一个元素是发送侧 -- 发送端,而第二个元素是接收侧 -- 接收端。由于历史原因,`tx` 和 `rx` 通常作为**发送端**(_transmitter_)和 **接收端**(_receiver_)的传统缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十九章会讨论 `let` 语句中的模式和解构。现在只需知道使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。 让我们将发送端移动到一个新建线程中并发送一个字符串,这样新建线程就可以和主线程通讯了,如示例 16-7 所示。这类似于在河的上游扔下一只橡皮鸭或从一个线程向另一个线程发送聊天信息: @@ -36,11 +35,11 @@ {{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-07/src/main.rs}} ``` -示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 “hi” +示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 `"hi"` -这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第九章复习正确处理错误的策略。 +这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过在一个真实应用中,需要合理地处理它:回到第九章复习正确处理错误的策略。 -在示例 16-8 中,我们在主线程中从信道的接收者获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息: +在示例 16-8 中,我们在主线程中从信道的接收端获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息: 文件名:src/main.rs @@ -48,15 +47,15 @@ {{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-08/src/main.rs}} ``` -示例 16-8: 在主线程中接收并打印内容 “hi” +示例 16-8: 在主线程中接收并打印内容 `"hi"` -信道的接收者有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写。这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。 +信道的接收端有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写,这会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。 `try_recv` 不会阻塞,相反它立刻返回一个 `Result`:`Ok` 值包含可用的信息,而 `Err` 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 `try_recv` 很有用:可以编写一个循环来频繁调用 `try_recv`,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。 出于简单的考虑,这个例子使用了 `recv`;主线程中除了等待消息之外没有任何其他工作,所以阻塞主线程是合适的。 -如果运行示例 16-8 中的代码,我们将会看到主线程打印出这个值: +运行示例 16-8 中的代码时,我们将会看到主线程打印出这个值: ```text Got: hi @@ -66,7 +65,7 @@ Got: hi ### 信道与所有权转移 -所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个试验来看看信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完 `val` 值 **之后** 再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的: +所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个实验来看看信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完 `val` 值**之后**再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的: 文件名:src/main.rs @@ -82,9 +81,9 @@ Got: hi {{#include ../listings/ch16-fearless-concurrency/listing-16-09/output.txt}} ``` -我们的并发错误会造成一个编译时错误。`send` 函数获取其参数的所有权并移动这个值归接收者所有。这可以防止在发送后再次意外地使用这个值;所有权系统检查一切是否合乎规则。 +我们的并发错误会造成一个编译时错误。`send` 函数获取其参数的所有权并移动这个值归接收端所有。这可以防止在发送后意外地再次使用这个值;所有权系统检查一切是否合乎规则。 -### 发送多个值并观察接收者的等待 +### 发送多个值并观察接收端的等待 示例 16-8 中的代码可以编译和运行,不过它并没有明确的告诉我们两个独立的线程通过信道相互通讯。示例 16-10 则有一些改进会证明示例 16-8 中的代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一秒钟。 @@ -96,7 +95,7 @@ Got: hi 示例 16-10: 发送多个消息,并在每次发送后暂停一段时间 -这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历它们,单独的发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。 +这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历它们,单独发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。 在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当信道被关闭时,迭代器也将结束。 @@ -111,9 +110,9 @@ Got: thread 因为主线程中的 `for` 循环里并没有任何暂停或等待的代码,所以可以说主线程是在等待从新建线程中接收值。 -### 通过克隆发送者来创建多个生产者 +### 通过克隆发送端来创建多个生产者 -之前我们提到了`mpsc`是 _multiple producer, single consumer_ 的缩写。可以运用 `mpsc` 来扩展示例 16-10 中的代码来创建向同一接收者发送值的多个线程。这可以通过克隆发送者来做到,如示例 16-11 所示: +之前我们提到了`mpsc`是 _multiple producer, single consumer_ 的缩写。可以运用 `mpsc` 来扩展示例 16-10 中的代码来创建多个向同一接收端发送值的线程。这可以通过克隆发送端来做到,如示例 16-11 所示: 文件名:src/main.rs @@ -123,9 +122,9 @@ Got: thread 示例 16-11: 从多个生产者发送多个消息 -这一次,在创建新线程之前,我们对发送者调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。 +这一次,在创建新线程之前,我们对发送端调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。 -如果运行这些代码,你 **可能** 会看到这样的输出: +运行代码时,输出应该看起来类似如下: ```text Got: hi diff --git a/src/ch16-03-shared-state.md b/src/ch16-03-shared-state.md index 6d247be..2ad2122 100644 --- a/src/ch16-03-shared-state.md +++ b/src/ch16-03-shared-state.md @@ -1,27 +1,26 @@ ## 共享状态的并发 -> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/main/src/ch16-03-shared-state.md) ->
-> commit 856d89c53a6d69470bb5669c773fdfe6aab6fcc9 + + -消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程访问同一块内存中的数据(共享状态)。再考虑一下 Go 语言文档中的这句口号:“不要通过共享内存来通讯”(“do not communicate by sharing memory.” +消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程访问相同的共享数据。再考虑一下 Go 语言文档中的这句口号:“不要通过共享内存来通讯”。(“do not communicate by sharing memory.”) -通过共享内存进行通信,会是什么样的代码?此外,为什么喜欢消息传递的人会警告:谨慎使用内存共享? +通过共享内存进行通信,会是什么样的代码?此外,为什么喜欢消息传递的人会警告说谨慎使用内存共享? -在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。在 15 章中,我们介绍了智能指针可以实现多所有权,然而这会增加额外的复杂性,因为需要管理多个所有者。Rust 的类型系统和所有权规则在正确管理这些问题上提供了极大的帮助:举个例子,让我们来看看 **互斥器**,一个较常见的共享内存并发原语。 +在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。在 15 章中,我们介绍了智能指针可以实现多所有权,然而这会增加额外的复杂性,因为需要管理多个所有者。Rust 的类型系统和所有权规则在正确管理这些问题上提供了极大的帮助:举个例子,让我们来看看互斥器(mutexes),较为常见的共享内存并发原语之一。 -### 使用互斥器,实现同一时刻只允许一个线程访问数据 +### 使用互斥器实现同一时刻只允许一个线程访问数据 -**互斥器**(_mutex_)是 互相排斥(_mutual exclusion_)的缩写。在同一时刻,其只允许一个线程对数据拥有访问权。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 **锁**(_lock_)来表明其希望访问数据。锁是一个数据结构,作为互斥器的一部分,它记录谁有数据的专属访问权。因此我们讲,互斥器通过锁系统 **保护**(_guarding_)其数据。 +**互斥器**(_mutex_)是互相排斥(_mutual exclusion_)的缩写,因为在同一时刻,它只允许一个线程访问数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的**锁**(_lock_)来表明其希望访问数据。锁是一个数据结构,作为互斥器的一部分,它记录谁有数据的专属访问权。因此我们讲,互斥器通过锁系统**保护**(_guarding_)其数据。 -互斥器以难以使用著称(译注:原文指互斥器在其他编程语言中难以使用),因为你必须记住: +互斥器以难以使用著称(译注:原文指互斥器在其他编程语言中难以使用),因为你必须记住两个规则: 1. 在使用数据之前,必须获取锁。 2. 使用完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。 作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。得到了麦克风后,他可以畅所欲言,讲完后再将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法正常进行! -正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。 +正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,你不可能在锁和解锁上出错。 ### `Mutex`的 API @@ -39,15 +38,15 @@ 如果另一个线程拥有锁,并且那个线程 panic 了,则 `lock` 调用会失败。在这种情况下,没人能够再获取锁,所以我们调用 `unwrap`,使当前线程 panic。 -一旦获取了锁,就可以将返回值(命名为 `num`)视为一个其内部数据(`i32`)的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁。`m` 的类型是 `Mutex` 而不是 `i32`,所以 **必须** 获取锁才能使用这个 `i32` 值。我们是不会忘记这么做的,因为如果没有获取锁,类型系统就不允许访问内部的 `i32` 值。 +一旦获取了锁,就可以将返回值(命名为 `num`)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁。`m` 的类型是 `Mutex` 而不是 `i32`,所以**必须**调用 `lock` 才能使用这个 `i32` 值。我们不能忘记这么做的;因为如果没有获取锁,类型系统就不允许访问内部的 `i32` 值。 -正如你所猜想的,`Mutex` 是一个智能指针。更准确的说,`lock` 调用 **返回** 一个叫做 `MutexGuard` 的智能指针。这个智能指针实现了 `Deref` 来指向其内部数据;它也实现了 `Drop`,当 `MutexGuard` 离开作用域时,自动释放锁(发生在示例 16-12 内部作用域的结尾)。有了这个特性,就不会有忘记释放锁的潜在风险(忘记释放锁会使互斥器无法再被其它线程使用),因为锁的释放是自动发生的。 +正如你所猜想的,`Mutex` 是一个智能指针。更准确的说,`lock` 调用会**返回**一个叫做 `MutexGuard` 的智能指针。`MutexGuard` 智能指针实现了 `Deref` 来指向其内部数据;它也实现了 `Drop`,当 `MutexGuard` 离开作用域时,自动释放锁(发生在示例 16-12 内部作用域的结尾)。这样一来,就不会有忘记释放锁从而导致互斥器阻塞无法被其他线程使用的潜在风险,因为锁的释放是自动发生的。 释放锁之后,我们可以打印出互斥器内部的 `i32` 值,并发现我们刚刚已经将其值改为 6。 -#### 在线程间共享 `Mutex` +#### 在多个线程间共享 `Mutex` -现在让我们尝试使用 `Mutex` 在多个线程间共享同一个值。我们将启动 10 个线程,并在各个线程中对同一个计数器值加 1,这样计数器将从 0 变为 10。示例 16-13 中的例子会出现编译错误,而我们将通过这些错误来学习如何使用 `Mutex`,以及 Rust 又是如何帮助我们正确使用的。 +现在让我们尝试使用 `Mutex` 在多个线程间共享同一个值。我们将启动 10 个线程,并在各个线程中对同一个计数器值加 1,这样计数器将从 0 累加到 10。示例 16-13 中的例子会出现编译错误,而我们将通过这些错误来学习如何使用 `Mutex`,以及 Rust 又是如何帮助我们正确使用它的。 文件名:src/main.rs @@ -57,11 +56,11 @@ 示例 16-13: 程序启动了 10 个线程,每个线程都通过 `Mutex` 来增加计数器的值 -这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex`,类似示例 16-12 那样。接下来我们遍历整数区间,创建了 10 个线程。我们使用了 `thread::spawn`,并为所有线程传入了相同的闭包:它们每一个都将调用 `lock` 方法来获取 `Mutex` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。 +这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex`,类似示例 16-12 那样。接下来我们遍历一个整数 range 来创建了 10 个线程。我们使用了 `thread::spawn`,并为所有线程传入了相同的闭包:它们每一个都将计数器移动进线程,调用 `lock` 方法来获取 `Mutex` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。 在主线程中,我们像示例 16-2 那样收集了所有的 `JoinHandle`,并调用它们的 `join` 方法来等待所有线程结束。然后,主线程会获取锁,并打印出程序的结果。 -之前提示过,这个例子不能编译,让我们看看为什么! +之前提示过,这个例子不能编译。现在让我们看看为什么! ```console {{#include ../listings/ch16-fearless-concurrency/listing-16-13/output.txt}} @@ -71,7 +70,7 @@ #### 多线程和多所有权 -在第 15 章中,我们用智能指针 `Rc` 来创建引用计数,使得一个值有了多个所有者。让我们做同样的事,看看会发生什么。将示例 16-14 中的 `Mutex` 封装进 `Rc` 中,并在将所有权移入线程之前克隆(clone) `Rc`。 +在第 15 章中,我们用智能指针 `Rc` 来创建引用计数,使得一个值有了多个所有者。让我们做同样的事,看看会发生什么。将示例 16-14 中的 `Mutex` 封装进 `Rc` 中,并在将所有权移入线程之前克隆(clone)`Rc`。 文件名:src/main.rs @@ -87,17 +86,18 @@ {{#include ../listings/ch16-fearless-concurrency/listing-16-14/output.txt}} ``` -哇哦,错误信息太长不看!划重点:第一行错误表明 `Rc>` 不能在线程间安全传递(`` `Rc>` cannot be sent between threads safely ``);编译器也指出了原因:`Rc>` 没有实现 `Send` trait(`` the trait `Send` is not implemented for `Rc>` ``)。下一节我们会讲到 `Send`:这是一个确保所使用的类型可以用于并发环境的 trait。 +哇哦,错误信息太长不看!下面是重点要关注的内容:`` `Rc>` cannot be sent between threads safely ``。编译器也指出了原因:`` the trait `Send` is not implemented for `Rc>` ``。下一节我们会讲到 `Send`:这是一个确保所使用的类型可以用于并发环境的 trait。 不幸的是,`Rc` 并不能安全的在线程间共享。当 `Rc` 管理引用计数时,它必须在每一个 `clone` 调用时增加计数,并在每一个克隆体被丢弃时减少计数。`Rc` 并没有使用任何并发原语,无法确保改变计数的操作不会被其他线程打断。这可能使计数出错,并导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。我们所需要的是一个与 `Rc` 完全一致,又以线程安全的方式改变引用计数的类型。 #### 原子引用计数 `Arc` -所幸 `Arc` 正是这么一个类似 `Rc` 并可以安全的用于并发环境的类型。字母 “a” 代表 **原子性**(_atomic_),所以这是一个 **原子引用计数**(_atomically reference counted_)类型。**原子类型** (Atomics) 是另一类这里还未涉及到的并发原语:请查看标准库中 [`std::sync::atomic`][atomic] 的文档来获取更多细节。目前我们只需要知道:原子类型就像基本类型一样,可以安全地在线程间共享。 +所幸 `Arc` 正是这么一个类似 `Rc` 并可以安全地用于并发环境的类型。字母 _a_ 代表 **原子性**(_atomic_),所以这是一个**原子引用计数**(_atomically reference counted_)类型。**原子类型** (Atomics) 是另一类这里还未涉及到的并发原语:请查看标准库中 [`std::sync::atomic`][atomic] 的文档来获取更多细节。目前我们只需要知道:原子类型就像基本类型一样,可以安全地在线程间共享。 -你可能会好奇,为什么不是所有的基本类型都是原子性的?为什么标准库中的类型没有全部默认使用 `Arc` 实现?原因在于,线程安全会造成性能损失,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,而不加入原子性可以使代码运行得更快。 +你可能会好奇,为什么不是所有的基本类型都是原子性的?为什么标准库中的类型没有全部默认使用 `Arc` 实现?原因在于,线程安全会造成性能损失,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,如果不必强制原子性所提供的保证可以使代码运行得更快。 -回到之前的例子:`Arc` 和 `Rc` 有着相同的 API,所以我们只需修改程序中的 `use` 行、`new` 调用和 `clone` 调用。示例 16-15 中的代码最终可以编译和运行: + +回到之前的例子:`Arc` 和 `Rc` 有着相同的 API,所以我们只需修改程序中的 `use` 行、`new` 调用和 `clone` 调用。示例 16-15 中的代码最终可以编译并运行。 文件名:src/main.rs @@ -107,13 +107,13 @@ 示例 16-15: 使用 `Arc` 包装一个 `Mutex` 能够实现在多线程之间共享所有权 -这会打印出: +这段代码会打印出如下: ```text Result: 10 ``` -成功了!我们从 0 数到了 10,这好像没啥大不了的,不过一路上我们确实学习了很多关于 `Mutex` 和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,我们可将计算任务分成独立的部分,并分散到多个线程中,接着使用 `Mutex` 使用各自的运算结果来更新最终的结果。 +成功了!我们从 0 数到了 10,这好像没啥大不了的,不过一路上我们确实学习了很多关于 `Mutex` 和线程安全的知识!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,我们可将计算任务分成独立的部分,并分散到多个线程中,接着使用 `Mutex` 使用各自的运算结果来更新最终的结果。 注意,对于简单的数值运算,[标准库中 `std::sync::atomic` 模块][atomic] 提供了比 `Mutex` 更简单的类型。针对基本类型,这些类型提供了安全、并发、原子的操作。在上面的例子中,为了专注于讲明白 `Mutex` 的用法,我们才选择在基本类型上使用 `Mutex`。(译注:对于上面例子中出现的 `i32` 加法操作,更好的做法是使用 `AtomicI32` 类型来完成。具体参考文档。) @@ -121,8 +121,8 @@ Result: 10 你可能注意到了,尽管 `counter` 是不可变的,我们仍然可以获取其内部值的可变引用;这意味着 `Mutex` 提供了内部可变性,就像 `Cell` 系列类型那样。使用 `RefCell` 可以改变 `Rc` 中内容(在 15 章中讲到过),同样地,使用 `Mutex` 我们也可以改变 `Arc` 中的内容。 -另一个值得注意的细节是,Rust 不能完全避免使用 `Mutex` 所带来的逻辑错误。回忆一下,第 15 章中讲过,使用 `Rc` 就有造成引用循环的风险:两个 `Rc` 值相互引用,造成内存泄漏。同理,`Mutex` 也有造成 **死锁**(_deadlock_)的风险:当某个操作需要锁住两个资源,而两个线程分别持有两个资源的其中一个锁时,它们会永远相互等待。如果你对这个话题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究别的语言中使用互斥器的死锁规避策略,并尝试在 Rust 中实现它们。标准库中 `Mutex` 和 `MutexGuard` 的 API 文档会提供有用的信息。 +另一个值得注意的细节是,Rust 不能完全避免使用 `Mutex` 所带来的逻辑错误。回忆一下,第 15 章中讲过,使用 `Rc` 就有造成引用循环的风险:两个 `Rc` 值相互引用,造成内存泄漏。同理,`Mutex` 也有造成**死锁**(_deadlock_)的风险:当某个操作需要锁住两个资源,而两个线程分别持有两个资源的其中一个锁时,它们会永远相互等待。如果你对这个话题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究其它语言中使用互斥器的死锁规避策略,并尝试在 Rust 中实现它们。标准库中 `Mutex` 和 `MutexGuard` 的 API 文档会提供有用的信息。 -接下来,为了丰富本章的内容,让我们讨论一下 `Send`和 `Sync` trait,以及如何对自定义类型使用它们。 +作为本章的最后,我们将讨论 `Send` 和 `Sync` trait,以及如何将它们用于自定义类型。 [atomic]: https://doc.rust-lang.org/std/sync/atomic/index.html diff --git a/src/ch16-04-extensible-concurrency-sync-and-send.md b/src/ch16-04-extensible-concurrency-sync-and-send.md index a814ef1..9cf4b46 100644 --- a/src/ch16-04-extensible-concurrency-sync-and-send.md +++ b/src/ch16-04-extensible-concurrency-sync-and-send.md @@ -1,18 +1,17 @@ ## 使用 `Send` 和 `Sync` trait 的可扩展并发 -> [ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/main/src/ch16-04-extensible-concurrency-sync-and-send.md) ->
-> commit 56ec353290429e6547109e88afea4de027b0f1a9 + + -Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。 +Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。处理并发的方案并不受标准库或语言所限:我们可以编写自己的或使用他人编写的并发特性。 -然而,在内嵌于语言而非标准库的关键并发概念中,有属于 `std::marker` 的 `Send` 和 `Sync` trait。 +然而,有一些关键的并发概念是内嵌于语言本身而非标准库的,其中就包括 `std::marker` 的 `Send` 和 `Sync` trait。 ### 通过 `Send` 允许在线程间转移所有权 `Send` 标记 trait 表明实现了 `Send` 的类型值的所有权可以在线程间传送。几乎所有的 Rust 类型都是`Send` 的,不过有一些例外,包括 `Rc`:这是不能实现 `Send` 的,因为如果克隆了 `Rc` 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,`Rc` 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。 -因此,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` 的,除了第二十章将会讨论的裸指针(raw pointer)。 @@ -20,7 +19,7 @@ Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所 `Sync` 标记 trait 表明一个实现了 `Sync` 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 `T`,如果 `&T`(`T` 的不可变引用)实现了 `Send` 的话 `T` 就实现了 `Sync`,这意味着其引用就可以安全的发送到另一个线程。类似于 `Send` 的情况,基本类型都实现了 `Sync`,完全由实现了 `Sync` 的类型组成的类型也实现了 `Sync`。 -智能指针 `Rc` 也没有实现 `Sync`,出于其没有实现 `Send` 相同的原因。`RefCell`(第十五章讨论过)和 `Cell` 系列类型没有实现 `Sync`。`RefCell` 在运行时所进行的借用检查也不是线程安全的。`Mutex` 实现了 `Sync`,正如 [“在线程间共享 `Mutex`”][sharing-a-mutext-between-multiple-threads] 部分所讲的它可以被用来在多线程中共享访问。 +智能指针 `Rc` 也没有实现 `Sync`,出于其没有实现 `Send` 相同的原因。`RefCell`(第十五章讨论过)和 `Cell` 系列类型没有实现 `Sync`。`RefCell` 在运行时所进行的借用检查也不是线程安全的。`Mutex` 实现了 `Sync`,正如 [“在多个线程间共享 `Mutex`”][sharing-a-mutext-between-multiple-threads] 部分所讲的它可以被用来在多线程中共享访问。 ### 手动实现 `Send` 和 `Sync` 是不安全的 @@ -34,7 +33,7 @@ Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所 正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。它们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。 -Rust 提供了用于消息传递的信道,和像 `Mutex` 和 `Arc` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧! +Rust 提供了用于消息传递的信道,和像 `Mutex` 和 `Arc` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确地运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发起来吧! -[sharing-a-mutext-between-multiple-threads]: ch16-03-shared-state.html#在线程间共享-mutext +[sharing-a-mutext-between-multiple-threads]: ch16-03-shared-state.html#在多个线程间共享-mutext [nomicon]: https://doc.rust-lang.org/nomicon/index.html diff --git a/src/ch17-02-concurrency-with-async.md b/src/ch17-02-concurrency-with-async.md index c6ea220..44cde4a 100644 --- a/src/ch17-02-concurrency-with-async.md +++ b/src/ch17-02-concurrency-with-async.md @@ -243,7 +243,7 @@ hi number 9 from the first task! {{#rustdoc_include ../listings/ch17-async-await/listing-17-13/src/main.rs:here}} ``` -
示例 17-13:通过多个异步代码块使用多个发送者
+
示例 17-13:通过多个异步代码块使用多个发送端
diff --git a/src/ch18-00-oop.md b/src/ch18-00-oop.md index 604df6d..0fa401f 100644 --- a/src/ch18-00-oop.md +++ b/src/ch18-00-oop.md @@ -1,7 +1,6 @@ -# Rust 的面向对象特性 +# 面向对象编程特性 -> [ch18-00-oop.md](https://github.com/rust-lang/book/blob/main/src/ch18-00-oop.md) ->
-> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892 + + -面向对象编程(Object-Oriented Programming,OOP)是一种对程序进行建模方式。对象(Object)作为一个编程概念来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构,该架构中对象之间互相传递消息。他在 1967 年创造了 **面向对象编程** (*object-oriented programming*)这个术语。关于 OOP 是什么有很多相互矛盾的定义;在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。 +面向对象编程(Object-Oriented Programming,OOP)是一种对程序进行建模方式。对象(Object)作为一个编程概念来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构,该架构中对象之间互相传递消息。他于 1967 年创造了**面向对象编程**(*object-oriented programming*)这一术语。对于 OOP 的定义众说纷纭;在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。