diff --git a/src/ch15-00-smart-pointers.md b/src/ch15-00-smart-pointers.md index 06936866..8844f123 100644 --- a/src/ch15-00-smart-pointers.md +++ b/src/ch15-00-smart-pointers.md @@ -1,17 +1,16 @@ # 智能指针 - - +[ch15-00-smart-pointers.md](https://github.com/rust-lang/book/blob/ecef81cbc6f0c2d1c8a67409329b0641258c04c2/src/ch15-00-smart-pointers.md) **指针**(*pointer*)是一个包含内存地址的变量的通用概念。这个地址会引用,或者说 “指向”(points at)一些其它数据。Rust 中最常见的指针是第四章介绍的**引用**(*reference*)。引用使用 `&` 符号来表示,而且会借用它们指向的值。它们除了引用数据之外没有任何其他特殊功能,也没有额外开销。 -另一方面,**智能指针**(*smart pointers*)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其它语言中。Rust 标准库中定义了多种不同的智能指针,它们提供了远超引用的功能。为了探索其基本概念,我们来看一些智能指针的例子,这包括**引用计数** (*reference counting*)智能指针类型。这种指针允许数据有多个所有者,它会记录所有者的数量,当没有所有者时清理数据。 +另一方面,**智能指针**(*smart pointers*)是一类数据结构,它们的行为类似指针,但还拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有:它起源于 C++,在其他语言中也存在。Rust 标准库中定义了多种不同的智能指针,它们提供了超出引用所能提供的功能。为了探索这一通用概念,我们会看几个不同的智能指针示例,其中包括**引用计数**(*reference counting*)智能指针类型。这种指针通过跟踪所有者的数量,让数据能够拥有多个所有者;当所有者都不存在时,它还会清理相应的数据。 -在 Rust 中因为引用和借用的概念,引用和智能指针有一个额外的区别:引用只会借用数据,而智能指针在很多时候**拥有**它们指向的数据。 +在 Rust 中,由于所有权和借用的概念,引用和智能指针之间还有一个额外区别:引用只会借用数据,而智能指针在很多情况下会**拥有**它们所指向的数据。 智能指针通常使用结构体实现。智能指针不同于普通结构体的地方在于它实现了 `Deref` 和 `Drop` trait。`Deref` trait 允许智能指针结构体实例表现得像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。`Drop` trait 允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么它们对智能指针很重要。 -考虑到智能指针是一个在 Rust 中经常使用的通用设计模式,本章的内容并不会涉及到所有现存的智能指针。很多库都有自己的智能指针,而且你也可以编写属于你自己的智能指针。这里会讲到标准库中最常用的几种: +考虑到智能指针模式是在 Rust 中经常使用的一种通用设计模式,本章不会覆盖所有现有的智能指针。很多库都有自己的智能指针,而且你甚至也可以编写自己的智能指针。这里会介绍标准库中最常用的几种: - `Box`,用于在堆上分配值 - `Rc`,一个引用计数类型,其数据可以有多个所有者 diff --git a/src/ch15-01-box.md b/src/ch15-01-box.md index 148ffd94..580037d6 100644 --- a/src/ch15-01-box.md +++ b/src/ch15-01-box.md @@ -1,23 +1,22 @@ ## 使用 `Box` 指向堆上的数据 - - +[ch15-01-box.md](https://github.com/rust-lang/book/blob/ecef81cbc6f0c2d1c8a67409329b0641258c04c2/src/ch15-01-box.md) -最简单直接的智能指针是 _box_,其类型是 `Box`。box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈与堆的区别请参考第四章。 +最简单直接的智能指针是 box,其类型写作 `Box`。Box 允许你将数据存储在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈和堆之间的区别,可以参考第四章。 -除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景: +除了把数据存储在堆上而不是栈上之外,box 没有性能开销。不过,它们也没有太多额外能力。你最常在以下这些场景中使用它们: - 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候 - 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候 - 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候 -我们会在[“box 允许创建递归类型”](#box-允许创建递归类型)部分展示第一种场景。在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(*trait object*),第十八章刚好有一整个部分[“顾及不同类型值的 trait 对象”][trait-objects]专门讲解这个主题。所以这里所学的内容会在第十八章再次应用! +我们会在[“Box 允许创建递归类型”](#box-允许创建递归类型)一节中展示第一种场景。在第二种情况下,转移大量数据的所有权可能会花费很长时间,因为数据会在栈上被复制。为了改善这种场景下的性能,我们可以把大量数据放进 box 中存储到堆上。这样,只有少量指针数据会在栈上被复制,而它所指向的数据则会一直留在堆上的同一位置。第三种情况被称为**trait 对象**(*trait object*),第十八章中的[“使用 trait 对象来抽象共享行为”][trait-objects]专门讨论了这个主题。所以你在这里学到的内容,还会在那一节中再次用到! -### 使用 `Box` 在堆上存储数据 +### 在堆上存储数据 在讨论 `Box` 的堆存储用例之前,让我们熟悉一下语法以及如何与存储在 `Box` 中的值进行交互。 -示例 15-1 展示了如何使用 box 在堆上储存一个 `i32`: +示例 15-1 展示了如何使用 box 在堆上存储一个 `i32` 值。 文件名:src/main.rs @@ -27,19 +26,19 @@ 示例 15-1:使用 box 在堆上储存一个 `i32` 值 -这里定义了变量 `b`,其值是一个指向被分配在堆上的值 `5` 的 `Box`。这个程序会打印出 `b = 5`;在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 `b` 这样的 box 在 `main` 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。 +我们将变量 `b` 定义为一个 `Box`,它指向值 `5`,而这个值被分配在堆上。这个程序会打印 `b = 5`;在这个例子里,我们访问 box 中数据的方式,和数据位于栈上时的方式类似。和任何拥有所有权的值一样,当 box 离开作用域时,就像 `b` 在 `main` 结束时那样,它会被释放。释放时既会清理 box 本身(存储在栈上),也会清理它指向的数据(存储在堆上)。 -将一个单独的值存放在堆上并不是很有意义,所以像示例 15-1 这样单独使用 box 并不常见。将像单个 `i32` 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。让我们看看一个不使用 box 时无法定义的类型的例子。 +把单个值放到堆上并没有太大意义,所以你不会经常像示例 15-1 那样单独使用 box。对于像单个 `i32` 这样的值来说,把它们放在默认存储位置栈上,在大多数情况下更合适。接下来,我们来看一个如果没有 box 就无法定义的类型。 ### Box 允许创建递归类型 **递归类型**(_recursive type_)的值可以拥有另一个同类型的值作为其自身的一部分。但是这会产生一个问题,因为 Rust 需要在编译时知道类型占用多少空间。递归类型的值嵌套理论上可以无限地进行下去,所以 Rust 不知道递归类型需要多少空间。因为 box 有一个已知的大小,所以通过在递归类型定义中插入 box,就可以创建递归类型了。 -作为一个递归类型的例子,让我们探索一下 _cons list_。这是一个函数式编程语言中常见的数据类型,来展示这个(递归类型)概念。除了递归之外,我们将要定义的 cons list 类型相当简单,所以这个例子中的概念,在任何遇到更为复杂的涉及到递归类型的场景时都很实用。 +作为递归类型的例子,让我们来看看 cons list。这是一种在函数式编程语言中常见的数据类型。我们将定义的 cons list 除了递归之外都很简单,因此这个例子里的概念,在你遇到更复杂的递归类型场景时也会很有用。 -#### cons list 的更多信息 +#### 理解 cons list -_cons list_ 是一个来源于 Lisp 编程语言及其方言的数据结构,它由嵌套的列表组成。它的名字来源于 Lisp 中的 `cons` 函数(“construct function" 的缩写),它利用两个参数来构造一个新的列表。通过对一个包含值的列表和另一个值调用 `cons`,可以构建由递归列表组成的 cons list。 +cons list 是一种来自 Lisp 编程语言及其方言的数据结构,由嵌套的 pair 组成,也是 Lisp 版本的链表。它的名字来源于 Lisp 中的 `cons` 函数(即 *construct function* 的缩写),这个函数用它的两个参数构造一个新的 pair。通过对一个由某个值和另一个 pair 组成的 pair 调用 `cons`,我们就能构造出由递归 pair 组成的 cons list。 例如这里有一个包含列表 `1, 2, 3` 的 cons list 的伪代码表示,其每个对在一个括号中: @@ -47,7 +46,7 @@ _cons list_ 是一个来源于 Lisp 编程语言及其方言的数据结构, (1, (2, (3, Nil))) ``` -cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值且没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的归基条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,它们代表无效或缺失的值。 +cons list 中的每一项都包含两个元素:当前项的值,以及下一项。列表中的最后一项只包含一个名为 `Nil` 的值,而没有下一项。cons list 是通过递归调用 `cons` 函数构造出来的。用来表示递归基例的规范名称是 `Nil`。注意,这和第六章讨论过的 “null” 或 “nil” 概念并不相同,后者表示无效或缺失的值。 cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型**确实**在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力地定义一个递归数据类型。 @@ -83,9 +82,9 @@ cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要 示例 15-4:尝试定义一个递归枚举时得到的错误 -这个错误表明这个类型 “有无限的大小”(“has infinite size”)。其原因是 `List` 的一个变体被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们拆开来看为何会得到这个错误。首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。 +这个错误表明,这个类型“有无限大小”。原因在于,我们把 `List` 的一个变体定义成了递归的:它直接持有另一个同类型的值。因此,Rust 无法判断存储一个 `List` 值到底需要多少空间。让我们拆开看看为什么会出现这个错误。首先,先来看看 Rust 是如何决定存储非递归类型的值需要多少空间的。 -### 计算非递归类型的大小 +#### 计算非递归类型的大小 回忆一下第六章讨论枚举定义时示例 6-2 中定义的 `Message` 枚举: @@ -95,13 +94,13 @@ cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要 当 Rust 需要知道要为 `Message` 值分配多少空间时,它可以检查每一个变体并发现 `Message::Quit` 并不需要任何空间,`Message::Move` 需要足够储存两个 `i32` 值的空间,依此类推。因为 enum 实际上只会使用其中的一个变体,所以 `Message` 值所需的空间等于储存其最大变体的空间大小。 -与此相对当 Rust 编译器检查像示例 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器从 `Cons` 变体开始分析,其需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,编译器检查其变体,从 `Cons` 变体开始。`Cons` 变体储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-1 所示: +与之相对的是,当 Rust 试图确定像示例 15-2 中 `List` 枚举这样的递归类型需要多少空间时,会发生什么。编译器先查看 `Cons` 变体,它持有一个 `i32` 类型的值和一个 `List` 类型的值。因此,`Cons` 所需的空间等于一个 `i32` 的大小再加上一个 `List` 的大小。为了算出 `List` 类型需要多少内存,编译器又要继续查看它的变体,并再次从 `Cons` 开始。`Cons` 又持有一个 `i32` 和一个 `List`,这个过程就会无限持续下去,如图 15-1 所示: An infinite Cons list 图 15-1:一个包含无限个 `Cons` 变体的无限 `List` -### 使用 `Box` 给递归类型一个已知的大小 +#### 获取一个已知大小的给递归类型 因为 Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了一个包括了有用建议的错误: @@ -112,9 +111,9 @@ help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle | ++++ + ``` -在建议中,_indirection_ 意味着不同于直接储存一个值,应该间接地存储一个指向值的指针。 +在这里,_indirection_ 的意思是:不要直接存储一个值,而是通过存储一个指向该值的指针,间接地存储它。 -因为 `Box` 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。这意味着可以将 `Box` 放入 `Cons` 变体中而不是直接存放另一个 `List` 值。`Box` 会指向下一个位于堆上的 `List` 值,而不是存放在 `Cons` 变体中。从概念上讲,我们仍然是在创建一个包含其他列表的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。 +因为 `Box` 是一个指针,Rust 总是知道 `Box` 需要多少空间:指针的大小并不会随它指向的数据量而变化。这意味着我们可以在 `Cons` 变体中放一个 `Box`,而不是直接再放一个 `List` 值。这个 `Box` 会指向下一个位于堆上的 `List` 值,而不是把这个 `List` 值直接放在 `Cons` 变体内部。从概念上说,我们仍然有一个“由列表组成的列表”,但现在这种实现更像是把这些项彼此相连,而不是一层层相互包含。 我们可以修改示例 15-2 中 `List` 枚举的定义和示例 15-3 中对 `List` 的应用,如示例 15-5 所示,这是可以编译的: @@ -132,8 +131,8 @@ help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle 图 15-2:因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了 -box 只提供了间接存储和堆分配;它们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。它们也没有这些特殊功能带来的性能损失,所以它们可以用于像 cons list 这样间接存储是唯一所需特性的场景。我们还将在第十八章看到 box 的更多应用场景。 +Box 只提供间接存储和堆分配;它没有我们将在其他智能指针类型中看到的那些额外特殊能力。它也没有那些特殊能力带来的性能开销,因此在像 cons list 这样我们只需要“间接存储”这一特性的场景里,Box 就很有用。我们还会在第十八章看到更多 Box 的用例。 -`Box` 类型是一个智能指针,因为它实现了 `Deref` trait,它允许 `Box` 值被当作引用对待。当 `Box` 值离开作用域时,由于 `Box` 类型 `Drop` trait 的实现,box 所指向的堆数据也会被清除。这两个 trait 对于在本章余下讨论的其他智能指针所提供的功能中,将会更为重要。让我们更详细地探索一下这两个 trait。 +`Box` 类型之所以是智能指针,是因为它实现了 `Deref` trait,这让 `Box` 的值可以像引用一样被处理。当 `Box` 的值离开作用域时,由于 `Drop` trait 的实现,box 指向的堆数据也会被清理掉。这两个 trait 对本章余下将讨论的其他智能指针类型所提供的功能会更加重要。接下来,我们更详细地看看这两个 trait。 [trait-objects]: ch18-02-trait-objects.html#顾及不同类型值的-trait-对象 diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md index 921af808..5893de15 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -1,13 +1,11 @@ -## 使用 `Deref` Trait 将智能指针当作常规引用处理 +## 将智能指针视作常规引用 - - +[ch15-02-deref.md](https://github.com/rust-lang/book/blob/ecef81cbc6f0c2d1c8a67409329b0641258c04c2/src/ch15-02-deref.md) -实现 `Deref` trait 允许我们定制**解引用运算符**(_dereference operator_)`*`(不要与乘法运算符或通配符相混淆)。通过这种方式实现 `Deref` trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并同样适用于智能指针。 +实现 `Deref` trait 允许你自定义**解引用运算符**(*dereference operator*)`*` 的行为(不要把它和乘法运算符或通配符运算符混淆)。通过以某种方式实现 `Deref`,使智能指针能够像常规引用一样被对待,你就可以编写操作引用的代码,并同样把它用于智能指针。 -让我们首先看看解引用运算符如何处理常规引用,接着尝试定义我们自己的类似 `Box` 的类型并看看为何解引用运算符不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **Deref 强制转换**(_deref coercions_)特性以及它是如何处理引用或智能指针的。 +先来看看解引用运算符是如何作用于常规引用的。然后,我们会尝试定义一个行为类似 `Box` 的自定义类型,并看看为什么解引用运算符在我们新定义的类型上不能像引用那样工作。接着,我们会探讨实现 `Deref` trait 如何让智能指针能够像引用一样工作。最后,我们会看看 Rust 的 **Deref 强制转换**(_deref coercions_)特性,以及它如何让我们既能处理引用,也能处理智能指针。 -> 我们将要构建的 `MyBox` 类型与真正的 `Box` 有一个很大的区别:我们的版本不会在堆上储存数据。这个例子重点关注 `Deref`,所以其数据实际存放在何处,相比其类似指针的行为来说不算重要。 ### 追踪引用的值 @@ -45,10 +43,12 @@ 示例 15-7 相比示例 15-6 主要不同的地方就是将 `y` 设置为一个指向 `x` 值拷贝的 `Box` 实例,而不是指向 `x` 值的引用。在最后的断言中,可以使用解引用运算符以 `y` 为引用时相同的方式追踪 `Box` 的指针。接下来让我们通过实现自己的类型来探索 `Box` 能这么做有何特殊之处。 -### 自定义智能指针 +### 定义我们自己的智能指针 为了体会默认情况下智能指针与引用的不同,让我们创建一个类似于标准库提供的 `Box` 类型的智能指针。接着学习如何增加使用解引用运算符的功能。 +> 注意:我们即将构建的 `MyBox` 类型与真正的 `Box` 有一个很大的区别:我们的版本不会把数据存储在堆上。因为这个示例关注的是 `Deref`,所以数据实际存储在哪里并不像这种“类似指针的行为”那样重要。 + 从根本上说,`Box` 被定义为包含一个元素的元组结构体,所以示例 15-8 以相同的方式定义了 `MyBox` 类型。我们还定义了 `new` 函数来对应定义于 `Box` 的 `new` 函数: 文件名:src/main.rs @@ -81,7 +81,7 @@ ### 实现 `Deref` trait -如第十章 [“为类型实现 trait”][impl-trait] 部分所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-10 包含定义于 `MyBox` 之上的 `Deref` 实现: +如第十章[“为类型实现 Trait”][impl-trait]所讨论的,为了实现 trait,我们需要为 trait 所要求的方法提供实现。标准库提供的 `Deref` trait 要求我们实现一个名为 `deref` 的方法,该方法借用 `self` 并返回一个指向内部数据的引用。示例 15-10 展示了要添加到 `MyBox` 定义上的 `Deref` 实现: 文件名:src/main.rs @@ -151,7 +151,7 @@ Deref 强制转换的加入使得 Rust 程序员编写函数和方法调用时 当所涉及到的类型定义了 `Deref` trait,Rust 会分析这些类型并使用任意多次 `Deref::deref` 调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时开销! -### Deref 强制转换如何与可变性交互 +### 处理可变引用的 Deref 强制转换 类似于如何使用 `Deref` trait 重载不可变引用的 `*` 运算符,Rust 提供了 `DerefMut` trait 用于重载可变引用的 `*` 运算符。 diff --git a/src/ch15-03-drop.md b/src/ch15-03-drop.md index 90cd1b09..ce96ff02 100644 --- a/src/ch15-03-drop.md +++ b/src/ch15-03-drop.md @@ -1,13 +1,12 @@ ## 使用 `Drop` Trait 运行清理代码 - - +[ch15-03-drop.md](https://github.com/rust-lang/book/blob/ecef81cbc6f0c2d1c8a67409329b0641258c04c2/src/ch15-03-drop.md) -对于智能指针模式来说第二个重要的 trait 是 `Drop`,其允许我们在值要离开作用域时自定义要执行的操作。你可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。 +对智能指针模式来说,第二个重要的 trait 是 `Drop`,它允许你自定义一个值即将离开作用域时要发生的事情。你可以为任何类型提供 `Drop` trait 的实现,而其中的代码可以用来释放诸如文件或网络连接之类的资源。 -我们在智能指针上下文中讨论 `Drop`,是因为在实现智能指针时几乎总会用到 `Drop` trait。例如,当 `Box` 被丢弃时会释放 box 指向的堆空间。 +我们在智能指针的上下文中介绍 `Drop`,是因为实现智能指针时几乎总会用到 `Drop` trait。例如,当一个 `Box` 被丢弃时,它会释放 box 所指向的堆空间。 -在其他一些语言中的某些类型,我们不得不记住在每次使用完那些类型的智能指针实例后调用清理内存或资源的代码。常见示例包括文件句柄(file handles)、套接字(sockets)和锁(locks)。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄漏资源! +在某些语言里,对于某些类型,程序员每次在使用完这些类型的实例后,都必须调用代码去释放内存或其他资源。常见例子包括文件句柄、套接字和锁。如果程序员忘了这么做,系统就可能因为负担过重而崩溃。在 Rust 中,你可以指定某段代码在值离开作用域时运行,而编译器会自动插入这段代码。这样一来,你就不必小心翼翼地在程序各处都放置清理代码来处理某个类型实例结束使用时的情况,同时也不会泄漏资源! 指定在值离开作用域时应该执行的代码的方式是实现 `Drop` trait。`Drop` trait 要求实现一个叫做 `drop` 的方法,它获取一个 `self` 的可变引用。为了能够看出 Rust 何时调用 `drop`,让我们暂时使用 `println!` 语句实现 `drop`。 @@ -31,7 +30,7 @@ {{#include ../listings/ch15-smart-pointers/listing-15-14/output.txt}} ``` -当实例离开作用域 Rust 会自动调用 `drop`,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃,所以 `d` 在 `c` 之前被丢弃。这个例子的作用是给了我们一个 drop 方法如何工作的可视化指导,不过通常需要指定类型所需执行的清理代码而不是打印信息。 +当实例离开作用域时,Rust 会自动替我们调用 `drop`,并运行我们指定的代码。变量会按照创建顺序的逆序被丢弃,所以 `d` 会先于 `c` 被丢弃。这个例子的目的,是让你直观地看到 `drop` 方法是如何工作的;而通常在真实代码里,你会写的是类型所需的清理逻辑,而不是一条打印消息。 不幸的是,禁用自动 `drop` 功能并不是一件容易的事。通常也不需要禁用 `drop` ;整个 `Drop` trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 `drop` 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 `Drop` trait 的 `drop` 方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 `std::mem::drop` 函数。 @@ -51,13 +50,13 @@ {{#include ../listings/ch15-smart-pointers/listing-15-15/output.txt}} ``` -错误信息表明不允许显式调用 `drop`。错误信息使用了术语**析构函数**(_destructor_),这是一个清理实例的函数的通用编程术语。**析构函数** 对应创建实例的**构造函数**(_constructor_)。Rust 中的 `drop` 函数就是这么一个析构函数。 +错误信息表明,我们不被允许显式调用 `drop`。错误信息中使用了术语**析构函数**(*destructor*),这是编程中对“清理某个实例的函数”的通用称呼。析构函数与**构造函数**(*constructor*)相对应,后者用于创建实例。Rust 中的 `drop` 函数就是一种特定的析构函数。 -Rust 不允许我们显式调用 `drop` 因为 Rust 仍然会在 `main` 的结尾对值自动调用 `drop`,这会导致一个 *double free* 错误,因为 Rust 会尝试清理相同的值两次。 +Rust 不允许我们显式调用 `drop`,因为 Rust 仍然会在 `main` 结束时自动对该值调用 `drop`。这会导致二次释放(*double free*)错误,因为 Rust 会尝试清理同一个值两次。 因为不能禁用当值离开作用域时自动插入的 `drop`,并且不能显式调用 `drop` 方法。如果我们需要强制提早清理值,可以使用 `std::mem::drop` 函数。 -`std::mem::drop` 函数不同于 `Drop` trait 中的 `drop` 方法。可以通过传递希望强制丢弃的值作为参数。该函数位于 prelude,所以我们可以修改示例 15-15 中的 `main` 来调用 `drop` 函数。如示例 15-16 所示: +`std::mem::drop` 函数与 `Drop` trait 中的 `drop` 方法不同。我们通过把想要强制提前丢弃的值作为参数传给它来调用。这个函数位于 prelude 中,因此我们可以修改示例 15-15 里的 `main`,改为调用 `drop` 函数,如示例 15-16 所示: 文件名:src/main.rs @@ -65,18 +64,18 @@ Rust 不允许我们显式调用 `drop` 因为 Rust 仍然会在 `main` 的结 {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-16/src/main.rs:here}} ``` -示例 15-16: 在值离开作用域之前调用 `std::mem::drop` 显式清理 +示例 15-16:在值离开作用域之前调用 `std::mem::drop` 显式清理 -运行这段代码会打印出如下: +运行这段代码将打印出如下内容: ```console {{#include ../listings/ch15-smart-pointers/listing-15-16/output.txt}} ``` -``Dropping CustomSmartPointer with data `some data`!`` 出现在 `CustomSmartPointer created.` 和 `CustomSmartPointer dropped before the end of main.` 之间,表明了 `drop` 方法被调用了并在此丢弃了 `c`。 +文本 ``Dropping CustomSmartPointer with data `some data`!`` 会出现在 `CustomSmartPointer created.` 和 `CustomSmartPointer dropped before the end of main.` 之间,这表明 `drop` 方法的代码在那个时刻被调用,以丢弃 `c`。 -`Drop` trait 实现中指定的代码可以用于多种场景,来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器!通过 `Drop` trait 和 Rust 所有权系统,你无需担心之后的代码清理,因为 Rust 会自动完成这些工作。 +你可以以多种方式利用 `Drop` trait 实现里指定的代码,让清理既方便又安全。例如,你可以用它来创建自己的内存分配器!有了 `Drop` trait 和 Rust 的所有权系统,你就不必记住何时进行清理,因为 Rust 会自动替你完成这些工作。 你也不必担心由于不小心清理仍在使用的值而导致的问题:所有权系统确保引用总是有效的,也会确保 `drop` 只会在值不再被使用时被调用一次。 -现在我们学习了 `Box` 和一些智能指针的特性,让我们聊聊标准库中定义的其他几种智能指针。 +现在我们已经了解了 `Box` 以及智能指针的一些特征,接下来看看标准库中定义的其他几种智能指针。 diff --git a/src/ch15-04-rc.md b/src/ch15-04-rc.md index 320daa28..0d80a848 100644 --- a/src/ch15-04-rc.md +++ b/src/ch15-04-rc.md @@ -1,7 +1,6 @@ ## `Rc` 引用计数智能指针 - - +[ch15-04-rc.md](https://github.com/rust-lang/book/blob/ecef81cbc6f0c2d1c8a67409329b0641258c04c2/src/ch15-04-rc.md) 大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点在没有任何边指向它从而没有任何所有者之前,都不应该被清理掉。 @@ -11,9 +10,9 @@ `Rc` 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。 -注意 `Rc` 只能用于单线程场景;在第十六章讨论并发时会涉及到如何在多线程程序中进行引用计数。 +注意,`Rc` 只能用于单线程场景。等到我们在第十六章讨论并发时,会介绍如何在多线程程序中进行引用计数。 -### 使用 `Rc` 共享数据 +### 共享数据 让我们回到示例 15-5 中使用 `Box` 定义 cons list 的例子。这一次,我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图 15-3 所示: @@ -39,11 +38,11 @@ {{#include ../listings/ch15-smart-pointers/listing-15-17/output.txt}} ``` -`Cons` 成员拥有其储存的数据,所以当创建 `b` 列表时,`a` 被移动进了 `b` 这样 `b` 就拥有了 `a`。接着当再次尝试使用 `a` 创建 `c` 时,这不被允许,因为 `a` 的所有权已经被移动。 +`Cons` 变体拥有它所持有的数据,因此当我们创建 `b` 列表时,`a` 会被移动进 `b`,于是 `b` 就拥有了 `a`。接着,当我们再次尝试在创建 `c` 时使用 `a`,这就不被允许了,因为 `a` 的所有权已经被移动。 可以改变 `Cons` 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。这是示例 15-17 中元素与列表的情况,但并不是所有情况都如此。 -相反,我们修改 `List` 的定义为使用 `Rc` 代替 `Box`,如示例 15-18 所示。现在每一个 `Cons` 变量都包含一个值和一个指向 `List` 的 `Rc`。当创建 `b` 时,不同于获取 `a` 的所有权,这里会克隆 `a` 所包含的 `Rc`,这会将引用计数从 1 增加到 2 并允许 `a` 和 `b` 共享 `Rc` 中数据的所有权。创建 `c` 时同样会克隆 `a`,这会将引用计数从 2 增加为 3。每次调用 `Rc::clone`,`Rc` 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。 +相反,我们会把 `List` 的定义改为使用 `Rc` 来代替 `Box`,如示例 15-18 所示。现在每个 `Cons` 变体都持有一个值,以及一个指向 `List` 的 `Rc`。当我们创建 `b` 时,不再获取 `a` 的所有权,而是克隆 `a` 持有的 `Rc`,从而把引用计数从 1 增加到 2,并让 `a` 和 `b` 共享这个 `Rc` 中数据的所有权。创建 `c` 时,我们也会克隆 `a`,使引用计数从 2 增加到 3。每次调用 `Rc::clone`,`Rc` 内部数据的引用计数都会增加,只有当引用数为零时,数据才会被清理。 文件名:src/main.rs @@ -55,9 +54,9 @@ 需要使用 `use` 语句将 `Rc` 引入作用域,因为它不在 prelude 中。在 `main` 中创建了存放 5 和 10 的列表并将其存放在 `a` 的新的 `Rc` 中。接着当创建 `b` 和 `c` 时,调用 `Rc::clone` 函数并传递 `a` 中 `Rc` 的引用作为参数。 -也可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,不过在这里 Rust 的习惯是使用 `Rc::clone`。`Rc::clone` 的实现并不像大部分类型的 `clone` 实现那样对所有数据进行深拷贝。`Rc::clone` 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。通过使用 `Rc::clone` 进行引用计数,可以在视觉上区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑 `Rc::clone` 调用。 +我们本来也可以调用 `a.clone()`,而不是 `Rc::clone(&a)`,但 Rust 的惯例是在这种情况下使用 `Rc::clone`。`Rc::clone` 的实现不像大多数类型的 `clone` 实现那样,会对所有数据进行深拷贝。调用 `Rc::clone` 只会增加引用计数,这并不耗时;而数据的深拷贝可能会花费不少时间。通过使用 `Rc::clone` 来表示引用计数操作,我们可以在视觉上区分“深拷贝式的 clone”和“只增加引用计数的 clone”。这样在查找性能问题时,我们只需要关注深拷贝那类 clone,而可以忽略 `Rc::clone` 调用。 -### 克隆 `Rc` 会增加引用计数 +### 克隆会增加引用计数 让我们修改示例 15-18 的代码以便观察创建和丢弃 `a` 中 `Rc` 的引用时引用计数的变化。 @@ -71,7 +70,7 @@ 示例 15-19:打印出引用计数 -在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 `Rc::strong_count` 函数获得。这个函数叫做 `strong_count` 而不是 `count` 是因为 `Rc` 也有 `weak_count`;在[“避免引用循环:将 `Rc` 变为 `Weak`”](ch15-06-reference-cycles.html#使用-weakt-防止引用循环)部分会讲解 `weak_count` 的用途。 +在程序中每个引用计数发生变化的地方,我们都会打印当前的引用计数,这个值是通过调用 `Rc::strong_count` 函数得到的。这个函数之所以叫 `strong_count` 而不是 `count`,是因为 `Rc` 还拥有 `weak_count`;我们会在[“使用 `Weak` 避免引用循环”][preventing-ref-cycles]一节中看到 `weak_count` 的用途。 这段代码会打印出: diff --git a/src/ch15-05-interior-mutability.md b/src/ch15-05-interior-mutability.md index 290072c2..575ee122 100644 --- a/src/ch15-05-interior-mutability.md +++ b/src/ch15-05-interior-mutability.md @@ -1,15 +1,14 @@ ## `RefCell` 和内部可变性模式 - - +[ch15-05-interior-mutability.md](https://github.com/rust-lang/book/blob/ecef81cbc6f0c2d1c8a67409329b0641258c04c2/src/ch15-05-interior-mutability.md) **内部可变性**(_Interior mutability_)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。不安全代码表明我们在手动检查这些规则而不是让编译器替我们检查。第二十章会更详细地介绍不安全代码。 当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 `unsafe` 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。 -让我们通过遵循内部可变性模式的 `RefCell` 类型来开始探索。 +让我们通过遵循内部可变性模式的 `RefCell` 类型来探索这个概念。 -### 通过 `RefCell` 在运行时强制借用规则 +### 在运行时强制借用规则 不同于 `Rc`,`RefCell` 代表其数据的唯一的所有权。那么是什么让 `RefCell` 不同于像 `Box` 这样的类型呢?回忆一下第四章所学的借用规则: @@ -34,7 +33,7 @@ 在不可变值内部改变值就是**内部可变性**(_interior mutability_)模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。 -### 内部可变性:不可变值的可变借用 +### 使用内部可变性 借用规则的一个推论是当有一个不可变值时,不能可变地借用它。例如,如下代码不能编译: @@ -52,7 +51,7 @@ 让我们通过一个实际的例子来探索何处可以使用 `RefCell` 来修改不可变值并看看为何这么做是有意义的。 -#### 内部可变性的用例:mock 对象 +#### 使用 mock 对象测试 有时在测试中程序员会用某个类型替换另一个类型,以便观察特定的行为并断言它是被正确实现的。这个占位符类型被称为 **测试替身**(_test double_)。就像电影制作中的替身演员(_stunt double_)一样,替代演员完成高难度的场景。测试替身在运行测试时替代某个类型。**mock 对象** 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。 @@ -95,7 +94,7 @@ Rust 并不像其他语言那样在标准库中提供内建的对象模型,Rus 不能修改 `MockMessenger` 来记录消息,因为 `send` 方法接收的是对 `self` 的不可变引用。我们也不能采纳错误提示中将 `&self` 改为 `&mut self` 的建议,因为那样既要在 `impl` 方法中修改签名,也要在 `Messenger` trait 定义中修改签名。我们并不希望仅为了测试而改变 `Messenger` trait。相反,我们需要想办法让测试代码与现有设计兼容,正常工作。 -这正是内部可变性的用武之地!我们将通过 `RefCell` 来储存 `sent_messages`,然后 `send` 将能够修改 `sent_messages` 并储存消息。示例 15-22 展示了代码: +这正是内部可变性可以派上用场的地方!我们会把 `sent_messages` 存储在一个 `RefCell` 里,然后 `send` 方法就能修改 `sent_messages` 来保存我们见到的消息。示例 15-22 展示了它的写法: 文件名:src/lib.rs @@ -113,7 +112,7 @@ Rust 并不像其他语言那样在标准库中提供内建的对象模型,Rus 现在我们见识了如何使用 `RefCell`,让我们研究一下它怎样工作的! -### `RefCell` 在运行时记录借用 +### 在运行时记录借用 当创建不可变和可变引用时,我们分别使用 `&` 和 `&mut` 语法。对于 `RefCell` 来说,则是 `borrow` 和 `borrow_mut` 方法,这属于 `RefCell` 安全 API 的一部分。`borrow` 方法返回 `Ref` 类型的智能指针,`borrow_mut` 方法返回 `RefMut` 类型的智能指针。这两个类型都实现了 `Deref`,所以可以当作常规引用对待。 @@ -137,13 +136,13 @@ Rust 并不像其他语言那样在标准库中提供内建的对象模型,Rus 注意代码 panic 和信息 `already borrowed: BorrowMutError`。这也就是 `RefCell` 如何在运行时处理违反借用规则的情况。 -像我们这里这样选择在运行时捕获借用错误而不是编译时意味着会发现在开发过程的后期才会发现的潜在错误,甚至有可能发布到生产环境才会发现。还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用 `RefCell` 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。虽然有取舍,但是我们可以选择使用 `RefCell` 来获得比常规引用所能提供的更多的功能。 +像这里这样选择在运行时而不是编译时捕获借用错误,意味着你可能会在开发过程的更后期才发现错误,甚至直到代码部署到生产环境后才暴露出来。与此同时,你的代码还会因为在运行时而不是编译时跟踪借用而承担一点运行时性能开销。不过,使用 `RefCell` 能让我们在只允许不可变值的上下文中,写出一个可以修改自身来记录消息的 mock 对象。尽管存在这些权衡,`RefCell` 仍能提供比常规引用更多的能力。 -### 结合 `Rc` 和 `RefCell` 来拥有多个可变数据所有者 +### 允许多个可变数据所有者 `RefCell` 的一个常见用法是与 `Rc` 结合。回忆一下 `Rc` 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 `RefCell` 的 `Rc` 的话,就可以得到有多个所有者**并且**可以修改的值了! -例如,回忆示例 15-18 的 cons list 的例子中使用 `Rc` 使得多个列表共享另一个列表的所有权。因为 `Rc` 只存放不可变值,所以一旦创建了这些列表值后就不能修改。让我们加入 `RefCell` 来获得修改列表中值的能力。示例 15-24 展示了通过在 `Cons` 定义中使用 `RefCell`,我们就允许修改所有列表中的值了: +例如,回忆示例 15-18 中的 cons list 例子,我们使用 `Rc` 让多个列表共享另一个列表的所有权。由于 `Rc` 只持有不可变值,所以一旦创建这些列表之后,就无法再修改其中的值。现在让我们加入 `RefCell`,借助它来修改列表中的值。示例 15-24 展示了:通过在 `Cons` 定义中使用 `RefCell`,我们就能修改所有列表中存储的值: 文件名:src/main.rs @@ -165,6 +164,6 @@ Rust 并不像其他语言那样在标准库中提供内建的对象模型,Rus {{#include ../listings/ch15-smart-pointers/listing-15-24/output.txt}} ``` -这个技巧非常巧妙!通过使用 `RefCell`,我们可以拥有一个表面上不可变的 `List`,不过可以使用 `RefCell` 中提供内部可变性的方法来在需要时修改数据。`RefCell` 的运行时借用规则检查也确实保护我们免于出现数据竞争,有时为了数据结构的灵活性而付出一些性能是值得的。注意 `RefCell` 不能用于多线程代码!`Mutex` 是一个线程安全版本的 `RefCell` ,我们会在第十六章讨论 `Mutex`。 +这个技巧相当巧妙!通过使用 `RefCell`,我们可以拥有一个对外看起来不可变的 `List` 值,但在需要时仍然能够使用 `RefCell` 提供的内部可变性方法来修改数据。借用规则在运行时进行检查,这也确实保护了我们免受数据竞争的影响;有时候,为了换取数据结构上的这种灵活性,付出一点性能代价是值得的。注意,`RefCell` 不适用于多线程代码!`Mutex` 是 `RefCell` 的线程安全版本,我们会在第十六章讨论 `Mutex`。 [wheres-the---operator]: ch05-03-method-syntax.html#--运算符到哪去了 diff --git a/src/ch15-06-reference-cycles.md b/src/ch15-06-reference-cycles.md index 77476931..cddcbd62 100644 --- a/src/ch15-06-reference-cycles.md +++ b/src/ch15-06-reference-cycles.md @@ -1,13 +1,12 @@ ## 引用循环与内存泄漏 - - +[ch15-06-reference-cycles.md](https://github.com/rust-lang/book/blob/ecef81cbc6f0c2d1c8a67409329b0641258c04c2/src/ch15-06-reference-cycles.md) Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 **内存泄漏**,_memory leak_),但并非不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 Rust 中被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出 Rust 允许出现内存泄漏:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。 ### 制造引用循环 -让我们看看引用循环是如何发生的以及如何避免它,以示例 15-25 中的 `List` 枚举和 `tail` 方法的定义开始: +让我们看看引用循环可能是如何发生的,以及如何避免它。先从示例 15-25 中 `List` 枚举和 `tail` 方法的定义开始: 文件名:src/main.rs @@ -15,11 +14,11 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-25/src/main.rs}} ``` -示例 15-25: 一个存放 `RefCell` 的 cons list 定义,这样可以修改 `Cons` 变体所引用的数据 +示例 15-25:一个持有 `RefCell` 的 cons list 定义,这样我们就能修改 `Cons` 变体所引用的内容 这里采用了示例 15-5 中 `List` 定义的另一种变体。现在 `Cons` 变体的第二个元素是 `RefCell>`,这意味着不同于像示例 15-24 那样能够修改 `i32` 的值,我们希望能够修改 `Cons` 变体所指向的 `List`。这里还增加了一个 `tail` 方法来方便我们在有 `Cons` 变体的时候访问其第二项。 -在示例 15-26 中增加了一个 `main` 函数,其使用了示例 15-25 中的定义。这些代码在 `a` 中创建了一个列表,一个指向 `a` 中列表的 `b` 列表,接着修改 `a` 中的列表指向 `b` 中的列表,这会创建一个引用循环。在这个过程的多个位置有 `println!` 语句展示引用计数。 +在示例 15-26 中,我们添加了一个 `main` 函数,它使用了示例 15-25 中的定义。这段代码会先在 `a` 中创建一个列表,再创建一个指向 `a` 中列表的 `b` 列表。然后,它会修改 `a` 中的列表,使其指向 `b`,从而创建一个引用循环。沿途加入的 `println!` 语句会展示这一过程中不同位置的引用计数。 文件:src/main.rs @@ -27,11 +26,11 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-26/src/main.rs:here}} ``` -示例 15-26:创建一个引用循环:两个 `List` 值互相指向彼此 +示例 15-26:创建两个彼此互相指向的 `List` 值,从而形成引用循环 -这里在变量 `a` 中创建了一个 `Rc` 实例来存放初值为 `5, Nil` 的 `List` 值。接着在变量 `b` 中创建了存放包含值 `10` 和指向列表 `a` 的 `List` 的另一个 `Rc` 实例。 +我们在变量 `a` 中创建了一个 `Rc` 实例,它持有一个值为 `5, Nil` 的 `List`。接着,又在变量 `b` 中创建了另一个 `Rc` 实例,它持有一个值为 `10`、并指向 `a` 中列表的 `List`。 -下来修改 `a` 使其指向 `b` 而不是 `Nil`,这就创建了一个循环。为此需要使用 `tail` 方法获取 `a` 中 `RefCell>` 的引用,并放入变量 `link` 中。接着使用 `RefCell>` 的 `borrow_mut` 方法将其值从存放 `Nil` 的 `Rc` 修改为 `b` 中的 `Rc`。 +然后,我们修改 `a`,让它指向 `b` 而不是 `Nil`,这样就创建了一个循环。为此,我们使用 `tail` 方法获取 `a` 中 `RefCell>` 的引用,并把它放到变量 `link` 中。接着,调用这个 `RefCell>` 上的 `borrow_mut` 方法,把它内部的值从持有 `Nil` 的 `Rc` 改成 `b` 中的 `Rc`。 如果保持最后的 `println!` 行注释并运行代码,会得到如下输出: @@ -39,11 +38,11 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 {{#include ../listings/ch15-smart-pointers/listing-15-26/output.txt}} ``` -可以看到将列表 `a` 修改为指向 `b` 之后, `a` 和 `b` 中的 `Rc` 实例的引用计数都是 2。在 `main` 的结尾,Rust 丢弃 `b`,这会使 `b` `Rc` 实例的引用计数从 2 减为 1。此时该 `Rc` 实例并不会被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 `a` 将 `a` `Rc` 实例的引用计数从 2 减为 1。这个实例也不能被回收,由于另一个 `Rc` 实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。为了更直观地展示这一引用循环,我们创建了一个如图 15-4 所示的示意图: +我们可以看到,当把 `a` 中的列表改为指向 `b` 之后,`a` 和 `b` 中 `Rc` 实例的引用计数都变成了 2。在 `main` 结束时,Rust 会先丢弃变量 `b`,这会使 `b` 中那个 `Rc` 实例的引用计数从 2 减到 1。由于引用计数不是 0,所以此时分配在堆上的内存不会被丢弃。然后,Rust 再丢弃 `a`,这会使 `a` 中那个 `Rc` 实例的引用计数也从 2 减到 1。这个实例的内存同样无法被清理,因为另一个 `Rc` 实例仍然引用着它。分配给这些列表的内存将会永远留在那里而不会被回收。为了更直观地展示这个引用循环,我们创建了图 15-4 所示的示意图: Reference cycle of lists -图 15-4: 列表 `a` 和 `b` 彼此互相指向形成引用循环 +图 15-4:列表 `a` 和 `b` 彼此互相指向,从而形成引用循环 如果取消最后 `println!` 的注释并运行程序,Rust 会尝试打印出 `a` 指向 `b` 指向 `a` 这样的循环直到栈溢出。 @@ -63,9 +62,9 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 作为示例,我们不再使用只知道下一个元素的列表,而是创建一个既知道子节点又知道父节点的树结构。 -#### 创建树形数据结构:带有子节点的 `Node` +#### 创建树形数据结构 -首先,我们将构建一个节点能够知道其子节点的树。创建一个用于存放其拥有所有权的 `i32` 值和对其子 `Node` 的引用: +首先,我们将构建一棵树,其中节点能够知道自己的子节点。我们会创建一个名为 `Node` 的结构体,它存放自己的 `i32` 值,以及对子 `Node` 值的引用: 文件名:src/main.rs @@ -73,7 +72,7 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-27/src/main.rs:here}} ``` -我们希望 `Node` 能够拥有其子节点,同时也希望能将所有权共享给变量,以便可以直接访问树中的每一个 `Node`,为此 `Vec` 的项的类型被定义为 `Rc`。我们还希望能修改其他节点的子节点,所以 `children` 中 `Vec>` 被放进了 `RefCell`。 +我们希望 `Node` 能拥有它的子节点,同时也希望能与变量共享这种所有权,以便能够直接访问树中的每个 `Node`。为此,我们将 `Vec` 中元素的类型定义为 `Rc`。我们还希望能够修改某个节点的子节点,因此把 `children` 中的 `Vec>` 包装进了 `RefCell`。 接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 `3` 且没有子节点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子节点的实例 `branch`,如示例 15-27 所示: @@ -91,7 +90,7 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 为了使子节点知道其父节点,需要在 `Node` 结构体定义中增加一个 `parent` 字段。问题是 `parent` 的类型应该是什么。我们知道其不能包含 `Rc`,因为这样 `leaf.parent` 将会指向 `branch` 而 `branch.children` 会包含 `leaf` 的指针,这会形成引用循环,会造成其 `strong_count` 永远也不会为 0。 -现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子! +换一种方式来思考这种关系:父节点应该拥有它的子节点。如果父节点被丢弃了,它的子节点也应该被丢弃。然而,子节点不应该拥有它的父节点。如果我们丢弃一个子节点,父节点仍然应该存在。这正是弱引用适用的场景! 所以 `parent` 使用 `Weak` 类型而不是 `Rc`,具体来说是 `RefCell>`。现在 `Node` 结构体定义看起来像这样: @@ -131,7 +130,7 @@ children: RefCell { value: [] } }] } }) 没有无限的输出表明这段代码并没有造成引用循环。这一点也可以从观察 `Rc::strong_count` 和 `Rc::weak_count` 调用的结果看出。 -#### 可视化 `strong_count` 和 `weak_count` 的改变 +#### 可视化 `strong_count` 和 `weak_count` 的变化 让我们通过创建了一个新的内部作用域并将 `branch` 的创建放入其中,来观察 `Rc` 实例的 `strong_count` 和 `weak_count` 值的变化。这会展示当 `branch` 创建和离开作用域被丢弃时会发生什么。这些修改如示例 15-29 所示: diff --git a/src/ch16-00-concurrency.md b/src/ch16-00-concurrency.md index ef3c7998..60fd0e6e 100644 --- a/src/ch16-00-concurrency.md +++ b/src/ch16-00-concurrency.md @@ -1,19 +1,18 @@ # 无畏并发 - - +[ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/9bd32402af8d3103302650895ec9d129ebfa47e1/src/ch16-00-concurrency.md) -安全且高效地处理并发编程是 Rust 的另一个主要目标。**并发编程**(_Concurrent programming_),代表程序的不同部分相互独立地执行,而**并行编程**(_parallel programming_)代表程序不同部分同时执行,这两个概念随着计算机越来越多的利用多处理器的优势而显得愈发重要。由于历史原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一现状。 +安全且高效地处理并发编程,是 Rust 的另一个主要目标。**并发编程**(*concurrent programming*)指程序的不同部分彼此独立地执行,而**并行编程**(*parallel programming*)则指程序的不同部分同时执行。随着越来越多的计算机开始利用多处理器的优势,这两个概念正变得越来越重要。历史上,在这些场景中编程一直都很困难且容易出错。Rust 希望改变这一点。 起初,Rust 团队认为确保内存安全和防止并发问题是两个分别需要不同方法应对的挑战。随着时间的推移,团队发现所有权和类型系统是一系列解决内存安全**和**并发问题的强有力的工具!通过利用所有权和类型检查,在 Rust 中很多并发错误都是**编译时**错误,而非运行时错误。因此,相比花费大量时间尝试重现运行时并发 bug 出现的特定情况,不正确的代码会直接编译失败并提供解释问题的错误信息。因此,你可以在开发时修复代码,而不是在部署到生产环境后修复代码。我们给 Rust 的这一部分起了一个绰号**无畏并发**(_fearless concurrency_)。无畏并发令你的代码免于出现诡异的 bug 并可以轻松重构且无需担心会引入新的 bug。 > 注意:出于简洁的考虑,我们将很多问题归类为**并发**,而不是更准确的区分**并发和/或并行**。对于本章,当我们谈到**并发**时,请自行脑内替换为 **并发和/或并行**。在下一章中当区分二者更为重要时,我们会使用更准确的表述。 -很多语言所提供的处理并发问题的解决方法都非常固有。例如,Erlang 有着优雅的消息传递(message-passing)并发功能,但只有模糊不清的在线程间共享状态的方法。对于高级语言来说,只实现可能解决方案的子集是一个合理的策略,因为高级语言所许诺的价值来源于牺牲一些控制来换取抽象。然而对于底层语言则期望提供在任何给定的情况下有着最高的性能且对硬件有更少的抽象。因此,Rust 提供了多种工具,以符合实际情况和需求的方式来为问题建模。 +很多语言对它们所提供的并发问题解决方案都比较“教条”。例如,Erlang 拥有优雅的消息传递并发功能,但在线程间共享状态方面只有一些不那么直观的方式。对于高级语言来说,只支持可能解决方案中的一个子集,是合理的策略,因为高级语言通过放弃一部分控制权来换取抽象,并承诺由此带来的收益。然而,底层语言通常被期望在任何给定场景下都能提供性能最佳的解决方案,并且对硬件施加更少抽象。因此,Rust 提供了多种工具,让你能够按照最适合自身场景和需求的方式来建模问题。 如下是本章将要涉及到的内容: - 如何创建线程来同时运行多段代码。 -- **消息传递**(_Message passing_)并发,其中信道(channel)被用来在线程间传递消息。 -- **共享状态**(_Shared state_)并发,其中多个线程可以访问同一片数据。 -- `Sync` 和 `Send` trait,将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。 +- **消息传递**(*message-passing*)并发,其中信道(channel)在线程之间发送消息。 +- **共享状态**(*shared-state*)并发,其中多个线程可以访问同一份数据。 +- `Sync` 和 `Send` trait,它们将 Rust 的并发保证扩展到用户定义类型以及标准库提供的类型。 diff --git a/src/ch16-01-threads.md b/src/ch16-01-threads.md index e481a83f..bc3ba064 100644 --- a/src/ch16-01-threads.md +++ b/src/ch16-01-threads.md @@ -1,9 +1,8 @@ ## 使用线程同时运行代码 - - +[ch16-01-threads.md](https://github.com/rust-lang/book/blob/9bd32402af8d3103302650895ec9d129ebfa47e1/src/ch16-01-threads.md) -在大部分现代操作系统中,已执行程序的代码在一个**进程**(_process_)中运行,操作系统则会负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为**线程**(_threads_)。例如,web 服务端可以有多个线程以便可以同时响应多个请求。 +在大多数当前的操作系统中,一个运行中的程序代码会在一个**进程**(*process*)中执行,而操作系统会同时管理多个进程。在一个程序内部,也可以存在彼此独立、同时运行的多个部分。运行这些独立部分的功能被称为**线程**(*threads*)。例如,一个 web 服务器可以拥有多个线程,以便同时响应多个请求。 将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题: @@ -13,7 +12,7 @@ Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编程仍需格外小心,同时其所要求的代码结构也不同于运行于单线程的程序。 -编程语言实现线程的方式各不相同,许多操作系统都提供了供语言调用以创建新线程的 API。Rust 标准库使用 *1:1* 模型的线程实现,这代表程序的每一个语言级线程使用一个系统线程。有一些 crate 实现了其他有着不同于 1:1 模型取舍的线程模型。(Rust 的 async 系统,我们将在下一章看到,也提供了另一种并发方式。) +编程语言实现线程的方式各不相同,许多操作系统都提供了可供编程语言调用、用来创建新线程的 API。Rust 标准库使用的是线程实现的 *1:1* 模型,也就是程序中的每一个语言级线程都对应一个操作系统线程。也有一些 crate 实现了其他线程模型,它们相对于 1:1 模型有着不同的取舍。(我们将在下一章看到的 Rust async 系统,也提供了另一种并发方式。) ### 使用 `spawn` 创建新线程 @@ -45,7 +44,7 @@ hi number 5 from the spawned thread! 如果运行代码只看到了主线程的输出,或没有出现重叠打印的现象,尝试增大区间 (变量 `i` 的范围) 来增加操作系统切换线程的机会。 -#### 使用 `join` 等待所有线程结束 +### 等待所有线程结束 由于主线程结束,示例 16-1 中的代码大部分时候不光会提早结束新建线程,因为无法保证线程运行的顺序,我们甚至不能实际保证新建线程会被执行! @@ -59,7 +58,7 @@ hi number 5 from the spawned thread! 示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle` 以确保该线程能够运行至结束 -通过调用句柄的 `join` 会阻塞当前线程直到句柄所代表的线程结束。**阻塞**(_Blocking_)线程意味着阻止该线程执行工作或退出。因为我们将 `join` 调用放在了主线程的 `for` 循环之后,运行示例 16-2 应该会产生类似这样的输出: +对句柄调用 `join` 会阻塞当前正在运行的线程,直到该句柄所代表的线程结束。**阻塞**(*blocking*)一个线程,意味着这个线程会被阻止继续工作或退出。因为我们把 `join` 调用放在主线程的 `for` 循环之后,运行示例 16-2 时应该会得到类似如下的输出: ```text hi number 1 from the main thread! @@ -111,8 +110,6 @@ hi number 4 from the main thread! `move` 关键字经常用于传递给 `thread::spawn` 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。在第十三章[“捕获环境”][capture]部分讨论了闭包上下文中的 `move`。现在我们会更专注于 `move` 和 `thread::spawn` 之间的交互。 -在第十三章中,我们讲到可以在参数列表前使用 `move` 关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。 - 注意示例 16-1 中传递给 `thread::spawn` 的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。示例 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作,如下所示: 文件名:src/main.rs diff --git a/src/ch16-02-message-passing.md b/src/ch16-02-message-passing.md index 4350a697..3ff0e4c5 100644 --- a/src/ch16-02-message-passing.md +++ b/src/ch16-02-message-passing.md @@ -1,7 +1,6 @@ ## 使用消息传递在线程间传送数据 - - +[ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/9bd32402af8d3103302650895ec9d129ebfa47e1/src/ch16-02-message-passing.md) 一个日益流行的确保安全并发的方式是**消息传递**(_message passing_),这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 [Go 编程语言文档](https://golang.org/doc/effective_go.html#concurrency) 中的口号:“不要通过共享内存来通讯;而要通过通讯来共享内存。”(“Do not communicate by sharing memory; instead, share memory by communicating.”) @@ -25,7 +24,7 @@ 这里使用 `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 所示。这类似于在河的上游扔下一只橡皮鸭或从一个线程向另一个线程发送聊天信息: @@ -63,7 +62,7 @@ Got: hi 完美! -### 信道与所有权转移 +### 通过信道转移所有权 所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个实验来看看信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完 `val` 值**之后**再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的: @@ -83,7 +82,7 @@ Got: hi 我们的并发错误会造成一个编译时错误。`send` 函数获取其参数的所有权并移动这个值归接收端所有。这可以防止在发送后意外地再次使用这个值;所有权系统检查一切是否合乎规则。 -### 发送多个值并观察接收端的等待 +### 发送多个值 示例 16-8 中的代码可以编译和运行,不过它并没有明确的告诉我们两个独立的线程通过信道相互通讯。示例 16-10 则有一些改进会证明示例 16-8 中的代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一秒钟。 @@ -110,9 +109,9 @@ Got: thread 因为主线程中的 `for` 循环里并没有任何暂停或等待的代码,所以可以说主线程是在等待从新建线程中接收值。 -### 通过克隆发送端来创建多个生产者 +### 创建多个生产者 -之前我们提到了`mpsc`是 _multiple producer, single consumer_ 的缩写。可以运用 `mpsc` 来扩展示例 16-10 中的代码来创建多个向同一接收端发送值的线程。这可以通过克隆发送端来做到,如示例 16-11 所示: +之前我们提到过,`mpsc` 是 *multiple producer, single consumer* 的缩写。现在就来实际用用这个特性,把示例 16-10 中的代码扩展为:创建多个线程,并都把值发送给同一个接收端。我们可以通过克隆发送端来做到这一点,如示例 16-11 所示: 文件名:src/main.rs diff --git a/src/ch16-03-shared-state.md b/src/ch16-03-shared-state.md index e325b116..4894d7da 100644 --- a/src/ch16-03-shared-state.md +++ b/src/ch16-03-shared-state.md @@ -1,7 +1,6 @@ -## 共享状态的并发 +## 共享状态并发 - - +[ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/9bd32402af8d3103302650895ec9d129ebfa47e1/src/ch16-03-shared-state.md) 消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程访问相同的共享数据。再考虑一下 Go 语言文档中的这句口号:“不要通过共享内存来通讯”。(“do not communicate by sharing memory.”) @@ -9,7 +8,7 @@ 在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。在 15 章中,我们介绍了智能指针可以实现多所有权,然而这会增加额外的复杂性,因为需要管理多个所有者。Rust 的类型系统和所有权规则在正确管理这些问题上提供了极大的帮助:举个例子,让我们来看看互斥器(mutexes),较为常见的共享内存并发原语之一。 -### 使用互斥器实现同一时刻只允许一个线程访问数据 +### 使用互斥器控制访问 **互斥器**(_mutex_)是互相排斥(_mutual exclusion_)的缩写,因为在同一时刻,它只允许一个线程访问数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的**锁**(_lock_)来表明其希望访问数据。锁是一个数据结构,作为互斥器的一部分,它记录谁有数据的专属访问权。因此我们讲,互斥器通过锁系统**保护**(_guarding_)其数据。 @@ -22,7 +21,7 @@ 正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,你不可能在锁和解锁上出错。 -### `Mutex`的 API +#### `Mutex` 的 API 我们先从在单线程环境中使用互斥器开始,作为展示其用法的一个例子,如示例 16-12 所示: @@ -44,7 +43,7 @@ 释放锁之后,我们可以打印出互斥器内部的 `i32` 值,并发现我们刚刚已经将其值改为 6。 -#### 在多个线程间共享 `Mutex` +#### 给 `Mutex` 共享访问 现在让我们尝试使用 `Mutex` 在多个线程间共享同一个值。我们将启动 10 个线程,并在各个线程中对同一个计数器值加 1,这样计数器将从 0 累加到 10。示例 16-13 中的例子会出现编译错误,而我们将通过这些错误来学习如何使用 `Mutex`,以及 Rust 又是如何帮助我们正确使用它的。 @@ -117,7 +116,7 @@ Result: 10 注意,对于简单的数值运算,[标准库中 `std::sync::atomic` 模块][atomic] 提供了比 `Mutex` 更简单的类型。针对基本类型,这些类型提供了安全、并发、原子的操作。在上面的例子中,为了专注于讲明白 `Mutex` 的用法,我们才选择在基本类型上使用 `Mutex`。(译注:对于上面例子中出现的 `i32` 加法操作,更好的做法是使用 `AtomicI32` 类型来完成。具体参考文档。) -### `RefCell`/`Rc` 与 `Mutex`/`Arc` 的相似性 +### 比较 `RefCell`/`Rc` 和 `Mutex`/`Arc` 你可能注意到了,尽管 `counter` 是不可变的,我们仍然可以获取其内部值的可变引用;这意味着 `Mutex` 提供了内部可变性,就像 `Cell` 系列类型那样。使用 `RefCell` 可以改变 `Rc` 中内容(在 15 章中讲到过),同样地,使用 `Mutex` 我们也可以改变 `Arc` 中的内容。 diff --git a/src/ch16-04-extensible-concurrency-sync-and-send.md b/src/ch16-04-extensible-concurrency-sync-and-send.md index 1fb723ac..fa3cd853 100644 --- a/src/ch16-04-extensible-concurrency-sync-and-send.md +++ b/src/ch16-04-extensible-concurrency-sync-and-send.md @@ -1,13 +1,12 @@ ## 使用 `Send` 和 `Sync` trait 的可扩展并发 - - +[ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/9bd32402af8d3103302650895ec9d129ebfa47e1/src/ch16-04-extensible-concurrency-sync-and-send.md) Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。处理并发的方案并不受标准库或语言所限:我们可以编写自己的或使用他人编写的并发特性。 然而,有一些关键的并发概念是内嵌于语言本身而非标准库的,其中就包括 `std::marker` 的 `Send` 和 `Sync` trait。 -### 通过 `Send` 允许在线程间转移所有权 +### 在线程间转移所有权 `Send` 标记 trait 表明实现了 `Send` 的类型值的所有权可以在线程间传送。几乎所有的 Rust 类型都是`Send` 的,不过有一些例外,包括 `Rc`:这是不能实现 `Send` 的,因为如果克隆了 `Rc` 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,`Rc` 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。 @@ -15,17 +14,17 @@ Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所 任何完全由 `Send` 的类型组成的类型也会自动被标记为 `Send`。几乎所有基本类型都是 `Send` 的,除了第二十章将会讨论的裸指针(raw pointer)。 -### `Sync` 允许多线程访问 +### 多线程访问 `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` 共享访问”][shared-access]中讲到的,它可以用来在多线程间共享访问。 ### 手动实现 `Send` 和 `Sync` 是不安全的 -通常并不需要手动实现 `Send` 和 `Sync` trait,因为完全由实现了 `Send` 和 `Sync` 的类型组成的类型,自动实现了 `Send` 和 `Sync`。因为它们是标记 trait,甚至都不需要实现任何方法。它们只是用来加强并发相关的不可变性的。 +通常并不需要手动实现 `Send` 和 `Sync` trait,因为完全由实现了 `Send` 和 `Sync` 的类型组成的类型,也会自动实现 `Send` 和 `Sync`。因为它们是标记 trait,甚至都不需要实现任何方法。它们只是用来强制执行与并发有关的不变性。 -手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,第二十章将会讲述具体的方法;当前重要的是,在创建新的由不是 `Send` 和 `Sync` 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[“The Rustonomicon”][nomicon] 中有更多关于这些保证以及如何维持它们的信息。 +手动实现这些 trait 需要编写 unsafe Rust 代码。第二十章会讲到如何使用 unsafe Rust;目前更重要的是,要知道如果构建新的并发类型,而它又不是完全由实现了 `Send` 和 `Sync` 的部分组成,就需要认真思考,以维持其安全保证。[“The Rustonomicon”][nomicon] 中有更多关于这些保证以及如何维持它们的信息。 ## 总结 @@ -35,5 +34,5 @@ Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所 Rust 提供了用于消息传递的信道,和像 `Mutex` 和 `Arc` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确地运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发起来吧! -[sharing-a-mutext-between-multiple-threads]: ch16-03-shared-state.html#在多个线程间共享-mutext +[shared-access]: ch16-03-shared-state.html#给-mutext-共享访问 [nomicon]: https://doc.rust-lang.org/nomicon/index.html diff --git a/src/ch17-00-async-await.md b/src/ch17-00-async-await.md index 7cad7015..2b233968 100644 --- a/src/ch17-00-async-await.md +++ b/src/ch17-00-async-await.md @@ -1,77 +1,75 @@ -# Async 和 await +# 异步编程基础:Async、Await、Future 与 Stream - - +[ch17-00-async-await.md](https://github.com/rust-lang/book/blob/0d5a0dd395aba1f82d7e5aaf6dbb59b2b843ad2c/src/ch17-00-async-await.md) -很多我们要求计算机处理的操作都需要一定的时间才能完成。例如,如果你使用视频编辑器来创建一个家庭聚会的视频,导出视频可能会花费几分钟到几小时不等。同样,从家庭成员那里下载共享的视频也可能需要很长时间。如果我们能在等待这些长时间运行的操作完成期间做点其他事情,那就太好了。 +很多我们要求计算机执行的操作都需要一段时间才能完成。如果在等待这些长时间运行的过程结束时,我们还能做点别的事情,就再好不过了。现代计算机提供了两种同时处理多个操作的技术:并行和并发。然而,我们的程序逻辑通常是以近乎线性的方式编写的。我们希望能够描述程序应执行的操作,以及函数在哪些点可以暂停并让程序的其他部分转而运行,而不需要预先精确指定每段代码究竟该以什么顺序、用什么方式运行。*异步编程* 正是一种抽象,它让我们能够用“可能暂停的点”和“最终结果”来表达代码,而协调执行的细节则由它来替我们处理。 -视频导出会尽可能使用所有的 CPU 和 GPU。如果你只有一个 CPU 核,同时操作系统在导出完成前也不会暂停,那么在其运行期间你无法使用计算机进行任何其他操作。这会是一个非常糟糕的体验。相反计算机的操作系统可以(也确实可以)隐式地中断导出过程,频率足够高,使你能够在导出进行的同时完成其他任务。 +本章以前一章通过线程实现并行和并发为基础,引入另一种编写代码的方式:Rust 的 futures、streams,以及 `async` 和 `await` 语法。它们让我们能够表达“操作可以是异步的”这一点;此外,还有由第三方 crate 提供的异步运行时,用来管理和协调这些异步操作的执行。 -下载文件则有所不同。它不占用大量的 CPU 时间。相反 CPU 需要等待来自于网络的数据。虽然可以在部分数据就绪时就开始读取,但等待剩余数据可能还需要一段时间。即便数据全部就绪了,视频文件也可能非常大,因此加载所有数据也会花费一些时间。虽然这可能只需要一两秒,不过这对于一个现代处理器来说已经是非常长的时间了,因为它每秒可以执行数十亿次操作。因此,如果能让 CPU 在等待网络调用完成的同时去处理别的工作就再好不过了。所以同上操作系统会隐式地中断你的程序以便其它工作可以在网络操作进行的同时继续进行。 +来看一个例子。假设你正在导出一个家庭聚会的视频,这个操作可能要花上几分钟甚至几小时。视频导出会尽可能多地占用 CPU 和 GPU 资源。如果你只有一个 CPU 核,而操作系统又不会在导出完成前暂停这个任务,也就是说它会以*同步*方式执行导出,那么在这个任务运行期间你就什么别的都做不了。这会是相当糟糕的体验。幸运的是,你的操作系统可以,而且确实会,足够频繁地打断导出任务,让你同时完成其他工作。 -> 注意:视频导出这类操作通常被称为 “CPU 密集型”(“CPU-bound”)或者 “计算密集型”(“compute-bound”)操作。其受限于计算机 *CPU* 或 *GPU* 处理数据的速度,以及它所能利用的计算能力。而下载视频这类操作通常被称为 “IO 密集型”(“IO-bound”)操作,因为其受限于计算机的 *输入输出* 速度。下载的速度最多只能与通过网络传输数据的速度一致。 +再假设你正在下载别人分享给你的视频。这个操作也可能耗时很长,但不会占用太多 CPU 时间。此时 CPU 主要是在等待网络数据到达。虽然数据一开始到达时你就可以开始读取,但全部数据到齐仍然可能需要一段时间。即便数据已经全部到达,如果视频文件很大,完整载入它也至少可能需要一两秒。听起来不算长,但对于每秒能执行数十亿次操作的现代处理器来说,这已经是很长的时间了。和前面的例子一样,操作系统也会在等待网络调用完成时悄悄打断程序,让 CPU 去处理其他工作。 + +视频导出属于 **CPU 密集型**(*CPU-bound*)或 **计算密集型**(*compute-bound*)操作:它受限于 CPU 或 GPU 处理数据的速度,以及操作能分配到多少计算能力。视频下载则属于 *I/O-bound* 操作,因为它受限于计算机的 *输入输出*(*input and output*)速度;它的速度最多只能和网络传输数据的速度一样快。 在上述两个例子中,操作系统的隐式中断提供了一种形式的并发。不过这种并发仅限于整个程序的级别:操作系统中断一个程序并让其它程序得以执行。在很多场景中,由于我们能比操作系统在更细粒度上理解我们的程序,因此我们可以观察到很多操作系统无法察觉的并发机会。 -例如,如果我们在构建一个管理文件下载的工具,我们应当以一种不会因开始一个下载任务而锁定 UI 的方式来编写程序,并且用户应该能够同时开始多个下载任务。不过很多操作系统与网络交互的 API 都是 *阻塞* 的(*blocking*)。也就是说这些 API 会阻塞程序的进程,直到它们处理的数据完全就绪。 +例如,如果我们正在构建一个管理文件下载的工具,程序就应该能做到:启动一个下载任务不会让 UI 卡死,而且用户还能够同时启动多个下载任务。不过,许多操作系统中与网络交互的 API 都是 *blocking* 的;也就是说,在它们所处理的数据完全就绪之前,会阻止程序继续向前执行。 + +> 注意:如果仔细想想,这其实也是*大多数*函数调用的工作方式。不过,*blocking* 这个术语通常保留给那些与文件、网络或计算机上其他资源交互的函数调用,因为正是在这些场景中,单个程序才会从 *non-blocking* 操作中明显受益。 -> 注意:如果你仔细思索一下,会发现这是 *大部分* 函数调用的工作方式!不过我们通常将 “阻塞” 这个术语保留给那些与文件、网络或其它计算机资源交互的函数调用,因为这些地方是单个程序可以从 *非* 阻塞操作中获益的地方。 +我们可以为每个文件单独创建一个线程来避免阻塞主线程。然而,这些线程所消耗的系统资源最终会成为问题。更理想的情况是:这些调用从一开始就不是阻塞的,并且我们只需定义程序想完成的一组任务,然后让运行时自行选择最佳的执行顺序和方式。 -我们可以新建专用的线程来下载每个文件以免阻塞主线程。然而,我们最终会发现这些线程的开销会成为一个问题。如果这些调用在一开始就是非阻塞的话那就更理想了。最后,如果我们能够像在阻塞代码中一样,以直接的风格编写非阻塞代码,那就更好了。比如这样: +这正是 Rust 的 *async*(*asynchronous* 的缩写)抽象所提供的能力。本章会介绍以下内容: -```rust,ignore,does_not_compile -let data = fetch_data_from(url).await; -println!("{data}"); -``` +- 如何使用 Rust 的 `async` 和 `await` 语法,并借助运行时执行异步函数 +- 如何用异步模型解决一些我们在第十六章中已经遇到过的挑战 +- 多线程和异步如何提供互补的解决方案,以及它们在许多场景下如何组合使用 -这正是 Rust 的 async 抽象所提供的。不过在讲解它们在实践中如何工作之前,让我们稍微绕个远路来了解一下并行(parallelism)和并发(concurrency)的区别。 +不过在看到 async 的实际工作方式之前,我们需要先稍微绕个远路,讨论一下并行(parallelism)和并发(concurrency)的区别。 -### 并行与并发 +## 并行与并发 在上一章中,我们大致将并行和并发视为可以互换的概念。但现在我们需要更加精确地区分它们,因为它们的区别将在实际工作中显现出来。 思考一下不同的团队分割方法来开发一个软件项目。我们可以分配给一个个人多个任务,也可以每个团队成员各自负责一个任务,或者可以采用这两种方法的组合。 -当一个个人在任何一个任务完成前同时处理多个任务,这就是 *并发*。你可能在计算机上同时运行两个项目,当你对其中一个项目感到厌倦或遇到困难时,可以切换到另一个项目。因为你是单独一个人,所以无法真正同时推进两个任务,但是你可以多任务处理,在不同任务之间切换以取得进展。 +当一个人在任何一个任务都还没完成之前,就在多个不同任务之间切换工作,这就是 *并发*。一种实现并发的方式,很像你在电脑上同时 checkout 了两个不同项目;当你对其中一个项目感到厌倦,或是在上面卡住时,就切到另一个。你只有一个人,所以不可能在完全相同的时刻同时推进两个任务,但你可以通过在它们之间切换来多任务处理,一次推进一个任务。
-并发工作流 +并发工作流
图 17-1:一个并发工作流,在任务 A 和任务 B 之间切换
-当你同意将一组任务在组员中分配,每一个组员分配一个任务并单独处理它,这就是 *并行*。每个组员可以真正同时进行工作。 +当团队把一组任务拆开,让每个成员各自负责一个任务并单独推进时,这就是 *并行*。团队中的每个人都可以在完全相同的时间取得进展。
-并发工作流 +并行工作流
图 17-2:一个并行流,其中任务 A 和任务 B 的工作同时独立进行
-在这两种场景中,你可能需要协调不同的任务。也许你 *认为* 某个人负责的任务与其他人的工作完全不相关,但实际上它确实依赖于团队中另一位成员的工作完成。一些工作可以并行进行,不过一些工作事实上是 *串行* 的:它们只能串行地发生,一个接着一个,如图 17-3 所示。 +在这两种工作流中,你都可能需要在不同任务之间做协调。也许你原以为分配给某个人的任务和其他人的工作完全独立,但实际上它必须等另一个人先完成自己的任务。有些工作可以并行完成,但其中一些实际上是 *串行* 的:它们只能按顺序发生,一个接一个,如图 17-3 所示。
-并发工作流 +部分并行工作流
图 17-3:一个部分并行的工作流,其中任务 A 和任务 B 的工作相互独立,直到任务 A3 阻塞在等待任务 B3 的结果
-同理,你可能会意识到你自己的一个任务依赖另一个任务。现在并发任务也变成串行的了。 +同样,你也可能意识到自己的一个任务依赖于另一个任务。那么你原本的并发工作也变成了串行。 -并行与并发也可能相互交叉(阻塞)。如果你得知某个同事卡在等待你的一个任务完成,你可能会集中所有精力在这个任务上来 “解锁” 你的同事。你和你的同事则不再能并行地工作了,同时你也不能够并发地处理自己的任务。 +并行和并发之间也可能彼此交叉。如果你得知某位同事正在卡着,必须等你先完成某个任务,那你很可能会把全部精力都集中到那个任务上,好“解除阻塞”。这时你和同事就不能再并行工作了,而你自己也不能再并发地推进其他任务。 同样的基础动态也作用于软件与硬件。在一个单核的机器上,CPU 一次只能执行一个操作,不过它仍然可以并发工作。借助像线程、进程和异步(async)等工具,计算机可以暂停一个活动,并在最终切换回第一个活动之前切换到其它活动。在一个有多个 CPU 核心的机器上,它也可以并行工作。一个核心可以做一件工作的同时另一个核心可以做一些完全不相关的工作,而且这些工作实际上是同时发生的。 -当使用 Rust 中的 async 时,我们总是在处理并发。取决于硬件、操作系统和所使用的异步运行时(async runtime)-- 稍后会介绍更多的异步运行时!并发也可能在底层使用了并行。 - -现在让我们深入理解 Rust 的异步编程实际上是如何工作的!在接下来的章节中,我们将: +在 Rust 中运行 async 代码时,通常是在并发地执行。至于这种并发在底层是否也会利用并行,则取决于硬件、操作系统,以及所使用的异步运行时(稍后我们会进一步介绍异步运行时)。 -- 学习如何使用 Rust 的 `async` 和 `await` 语法 -- 探索如何使用异步模型来解决第十六章中遇到的一些挑战 -- 了解多线程和异步如何互补,在很多场景中你甚至可以同时使用两者 +现在,让我们深入看看 Rust 中的异步编程究竟是如何工作的。 diff --git a/src/ch20-02-advanced-traits.md b/src/ch20-02-advanced-traits.md index 619104e5..2b8b5b46 100644 --- a/src/ch20-02-advanced-traits.md +++ b/src/ch20-02-advanced-traits.md @@ -266,12 +266,12 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法 `Display` 的实现使用 `self.0` 来访问其内部的 `Vec`,因为 `Wrapper` 是元组结构体而 `Vec` 是结构体总位于索引 0 的项。接着就可以使用 `Wrapper` 中 `Display` 的功能了。 -这种做法的缺点在于因为 `Wrapper` 是一个新类型,它并不具备其所封装值的方法。必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上,这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 [“使用 `Deref` Trait 将智能指针当作常规引用处理”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则只需自行实现所需的方法即可。 +这种做法的缺点在于因为 `Wrapper` 是一个新类型,它并不具备其所封装值的方法。必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上,这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 [“将智能指针视作常规引用”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则只需自行实现所需的方法即可。 甚至当不涉及 trait 时 newtype 模式也很有用。现在让我们将关注点转移到一些与 Rust 类型系统交互的高级方式上来吧。 [newtype]: ch20-02-advanced-traits.html#使用-newtype-模式在外部类型上实现外部-trait [implementing-a-trait-on-a-type]: ch10-02-traits.html#为类型实现-trait [traits-defining-shared-behavior]: ch10-02-traits.html#trait定义共同行为 -[smart-pointer-deref]: ch15-02-deref.html#使用-deref-trait-将智能指针当作常规引用处理 +[smart-pointer-deref]: ch15-02-deref.html#将智能指针视作常规引用 [tuple-structs]: ch05-01-defining-structs.html#使用元组结构体创建不同的类型