diff --git a/README.md b/README.md index 12a3ee9..4631943 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Rust 程序设计语言(第二版) 简体中文版 -还在施工中:正在翻译第十六章 +还在施工中:正在翻译第十六章第二部分 -目前正在解决代码排版问题:已检查到第四章 \ No newline at end of file +目前正在解决代码排版问题:已检查到第五章 \ No newline at end of file diff --git a/docs/ch03-02-data-types.html b/docs/ch03-02-data-types.html index 8e706ae..47f40e9 100644 --- a/docs/ch03-02-data-types.html +++ b/docs/ch03-02-data-types.html @@ -167,7 +167,7 @@ commit 04aa3a45eb72855b34213703718f50a12a3eeec8

复合类型

复合类型可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

将值组合进元组

-

元组是一个将多个其他类型的值组合进一个复合类型的组要方式。

+

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。

我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这写不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:

Filename: src/main.rs

fn main() {
diff --git a/docs/ch05-00-structs.html b/docs/ch05-00-structs.html
index e451f79..308bf7c 100644
--- a/docs/ch05-00-structs.html
+++ b/docs/ch05-00-structs.html
@@ -71,12 +71,11 @@
 

ch05-00-structs.md
-commit 255b44b409585e472e14c396ebc75d28f540a1ac

+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

-

struct,是 structure 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,struct就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定行为的函数。structenum(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查,来在程序范围创建新类型的基本组件。

-

对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命令各部分数据所以能更清楚的知道其值是什么意思。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。

-

为了定义结构体,通过struct关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作字段fields),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体:

-
+

struct,是 structure 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,struct就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定行为的函数。structenum(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查来在程序范围内创建新类型的基本组件。

+

对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命名各部分数据以便能更清楚的知道其值的意义。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。

+

为了定义结构体,通过struct关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作字段field),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体:

struct User {
     username: String,
     email: String,
@@ -84,11 +83,8 @@ commit 255b44b409585e472e14c396ebc75d28f540a1ac

active: bool, }
-
-

Listing 5-1: A User struct definition

-
-
-

一旦定义后为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用key: value对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板。例如,我们可以像这样来声明一个特定的用户:

+

Listing 5-1: A User struct definition

+

一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用key: value对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,我们可以像这样来声明一个特定的用户:

# struct User {
 #     username: String,
 #     email: String,
@@ -141,8 +137,7 @@ error[E0106]: missing lifetime specifier
 

一个示例程序

为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。

使用 Cargo 来创建一个叫做 rectangles 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 src/main.rs 文件中为此实现的一个小程序:

-
-Filename: src/main.rs +

Filename: src/main.rs

fn main() {
     let length1 = 50;
     let width1 = 30;
@@ -157,23 +152,19 @@ fn area(length: u32, width: u32) -> u32 {
     length * width
 }
 
-
-

Listing 5-2: Calculating the area of a rectangle specified by its length and -width in separate variables

-
-
+

Listing 5-2: Calculating the area of a rectangle +specified by its length and width in separate variables

尝试使用cargo run运行程序:

The area of the rectangle is 1500 square pixels.
 

使用元组重构

-

我们的小程序能正常运行;它调用area函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们一起才能定义一个长方形。

+

我们的小程序能正常运行;它调用area函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们在一起才能定义一个长方形。

这个做法的问题突显在area的签名上:

fn area(length: u32, width: u32) -> u32 {
 

函数area本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。

第三章已经讨论过了一种可行的方法:元组。列表 5-3 是一个使用元组的版本:

-
-Filename: src/main.rs +

Filename: src/main.rs

fn main() {
     let rect1 = (50, 30);
 
@@ -187,21 +178,18 @@ fn area(dimensions: (u32, u32)) -> u32 {
     dimensions.0 * dimensions.1
 }
 
-
-

Listing 5-3: Specifying the length and width of the rectangle with a tuple

-
-
+

Listing 5-3: Specifying the length and width of the +rectangle with a tuple

-

在某种程度上说这样好一点了。元组帮助我们增加了一些结构,现在在调用area的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:

+

在某种程度上说这样好一点了。元组帮助我们增加了一些结构性,现在在调用area的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:

dimensions.0 * dimensions.1
 
-

在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引0length1width。如果其他人要使用这些代码,他们也不得不搞清楚后再记住。容易忘记或者混淆这些值而造成错误,因为我们没有表达我们代码中数据的意义。

+

在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引0length1width。如果其他人要使用这些代码,他们也不得不搞清楚后再记住他们。容易忘记或者混淆这些值而造成错误,因为我们没有表明代码中数据的意义。

使用结构体重构:增加更多意义

-

现在引入结构体。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示:

-
-Filename: src/main.rs +

现在引入结构体的时候了。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示:

+

Filename: src/main.rs

struct Rectangle {
     length: u32,
     width: u32,
@@ -220,18 +208,14 @@ fn area(rectangle: &Rectangle) -> u32 {
     rectangle.length * rectangle.width
 }
 
-
-

Listing 5-4: Defining a Rectangle struct

-
-
+

Listing 5-4: Defining a Rectangle struct

这里我们定义了一个结构体并称其为Rectangle。在{}中定义了字段lengthwidth,都是u32类型的。接着在main中,我们创建了一个长度为 50 和宽度为 30 的Rectangle的具体实例。

函数area现在被定义为接收一个名叫rectangle的参数,它的类型是一个结构体Rectangle实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样main函数就可以保持rect1的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有&

-

area函数访问Rectanglelengthwidth字段。area的签名现在明确的表明了我们的意图:计算一个Rectangle的面积,通过其lengthwidth字段。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值01。这是明确性的胜利。

+

area函数访问Rectanglelengthwidth字段。area的签名现在明确的表明了我们的意图:通过其lengthwidth字段,计算一个Rectangle的面积,。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值01。这是明确性的胜利。

通过衍生 trait 增加实用功能

如果能够在调试程序时打印出Rectangle实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用println!宏:

-
-Filename: src/main.rs +

Filename: src/main.rs

struct Rectangle {
     length: u32,
     width: u32,
@@ -243,10 +227,8 @@ fn main() {
     println!("rect1 is {}", rect1);
 }
 
-
-

Listing 5-5: Attempting to print a Rectangle instance

-
-
+

Listing 5-5: Attempting to print a Rectangle +instance

如果运行代码,会出现带有如下核心信息的错误:

error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
 
@@ -264,7 +246,6 @@ fn main() { crate, add `#[derive(Debug)]` or manually implement it

Rust 确实包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上#[derive(Debug)]注解,如列表 5-6 所示:

-
#[derive(Debug)]
 struct Rectangle {
     length: u32,
@@ -277,11 +258,8 @@ fn main() {
     println!("rect1 is {:?}", rect1);
 }
 
-
-

Listing 5-6: Adding the annotation to derive the Debug trait and printing the -Rectangle instance using debug formatting

-
-
+

Listing 5-6: Adding the annotation to derive the Debug +trait and printing the Rectangle instance using debug formatting

此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出:

rect1 is Rectangle { length: 50, width: 30 }
 
diff --git a/docs/ch05-01-method-syntax.html b/docs/ch05-01-method-syntax.html index b85e850..3c19190 100644 --- a/docs/ch05-01-method-syntax.html +++ b/docs/ch05-01-method-syntax.html @@ -71,13 +71,12 @@

ch05-01-method-syntax.md
-commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

+commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2

-

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十三章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

+

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

定义方法

让我们将获取一个Rectangle实例作为参数的area函数改写成一个定义于Rectangle结构体上的area方法,如列表 5-7 所示:

-
-Filename: src/main.rs +

Filename: src/main.rs

#[derive(Debug)]
 struct Rectangle {
     length: u32,
@@ -99,15 +98,13 @@ fn main() {
     );
 }
 
-
-

Listing 5-7: Defining an area method on the Rectangle struct

-
-
+

Listing 5-7: Defining an area method on the Rectangle +struct

为了使函数定义于Rectangle的上下文中,我们开始了一个impl块(implimplementation 的缩写)。接着将函数移动到impl大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成self。然后在main中将我们调用area方法并传递rect1作为参数的地方,改成使用方法语法Rectangle实例上调用area方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。

area的签名中,开始使用&self来替代rectangle: &Rectangle,因为该方法位于impl Rectangle 上下文中所以 Rust 知道self的类型是Rectangle。注意仍然需要在self前面加上&,就像&Rectangle一样。方法可以选择获取self的所有权,像我们这里一样不可变的借用self,或者可变的借用self,就跟其他别的参数一样。

这里选择&self跟在函数版本中使用&Rectangle出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将抵押给参数改为&mut self。通过仅仅使用self作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。

-

使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl块中,而不是让将来的用户在我们的代码中到处寻找`Rectangle的功能。

+

使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl块中,而不是让将来的用户在我们的代码中到处寻找Rectangle的功能。

->运算符到哪去了?

@@ -138,8 +135,7 @@ p1.distance(&p2);

带有更多参数的方法

让我们更多的实践一下方法,通过为Rectangle结构体实现第二个方法。这回,我们让一个Rectangle的实例获取另一个Rectangle实例并返回self能否完全包含第二个长方形,如果能返回true若不能则返回false。当我们定义了can_hold方法,就可以运行列表 5-8 中的代码了:

-
-Filename: src/main.rs +

Filename: src/main.rs

fn main() {
     let rect1 = Rectangle { length: 50, width: 30 };
     let rect2 = Rectangle { length: 40, width: 10 };
@@ -149,15 +145,13 @@ p1.distance(&p2);
     println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
 }
 
-
-

Listing 5-8: Demonstration of using the as-yet-unwritten can_hold method

-
-
+

Listing 5-8: Demonstration of using the as-yet-unwritten +can_hold method

我们希望看到如下输出,因为rect2的长宽都小于rect1,而rect3rect1要宽:

Can rect1 hold rect2? true
 Can rect1 hold rect3? false
 
-

因为我们想定义一个方法,所以它应该位于impl Rectangle块中。方法名是can_hold,并且它会获取另一个Rectangle的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:rect1.can_hold(&rect2)传入了&rect2,它是一个Rectangle的实例rect2的不可变借用。这是可以理解的,因为我们只需要读取rect2(而不是写入,这意味着我们需要一个可变借用)而且希望main保持rect2的所有权这样就可以在调用这个方法后继续使用它。can_hold的返回值是一个布尔值,其实现会分别检查self的长宽是够都大于另一个Rectangle。让我们在列表 5-7 的impl块中增加这个新方法:

+

因为我们想定义一个方法,所以它应该位于impl Rectangle块中。方法名是can_hold,并且它会获取另一个Rectangle的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:rect1.can_hold(&rect2)传入了&rect2,它是一个Rectangle的实例rect2的不可变借用。这是可以理解的,因为我们只需要读取rect2(而不是写入,这意味着我们需要一个可变借用)而且希望main保持rect2的所有权这样就可以在调用这个方法后继续使用它。can_hold的返回值是一个布尔值,其实现会分别检查self的长宽是够都大于另一个Rectangle。让我们在列表 5-7 的impl块中增加这个新方法,如列表 5-9 所示:

Filename: src/main.rs

# #[derive(Debug)]
 # struct Rectangle {
@@ -175,6 +169,8 @@ impl Rectangle {
     }
 }
 
+

Listing 5-9: Implementing the can_hold method on +Rectangle that takes another Rectangle instance as an argument

如果结合列表 5-8 的main函数来运行,就会看到想要得到的输出!方法可以在self后增加多个参数,而且这些参数就像函数中的参数一样工作。

关联函数

diff --git a/docs/ch06-00-enums.html b/docs/ch06-00-enums.html index 73e3dfc..0057196 100644 --- a/docs/ch06-00-enums.html +++ b/docs/ch06-00-enums.html @@ -71,7 +71,7 @@

ch06-00-enums.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+commit 4f2dc564851dc04b271a2260c834643dfd86c724

本章介绍枚举,也被称作 enums。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做Option,它代表一个值要么是一些值要么什么都不是。然后会讲到match表达式中的模式匹配如何使对枚举不同的值运行不同的代码变得容易。最后会涉及到if let,另一个简洁方便处理代码中枚举的结构。

枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像F#、OCaml 和 Haskell这样的函数式编程语言中的代数数据类型algebraic data types)最为相似。

diff --git a/docs/ch06-01-defining-an-enum.html b/docs/ch06-01-defining-an-enum.html index 8aff8d7..bfc2c27 100644 --- a/docs/ch06-01-defining-an-enum.html +++ b/docs/ch06-01-defining-an-enum.html @@ -71,7 +71,7 @@

ch06-01-defining-an-enum.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+commit e6d6caab41471f7115a621029bd428a812c5260e

让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以枚举出所有可能的值,这也正是它名字的由来。

任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值尽可能是其一个成员。IPv4 和 IPv6 从根本上讲都是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。

diff --git a/docs/ch16-01-threads.html b/docs/ch16-01-threads.html index 3ea00c4..ddb3896 100644 --- a/docs/ch16-01-threads.html +++ b/docs/ch16-01-threads.html @@ -75,7 +75,206 @@ commit 55b294f20fc846a13a9be623bf322d8b364cee77

在今天使用的大部分操作系统中,当程序执行时,操作系统运行代码的上下文称为进程process)。操作系统可以运行很多进程,而操作系统也管理这些进程使得多个程序可以在电脑上同时运行。

我们可以将每个进程运行一个程序的概念再往下抽象一层:程序也可以在其上下文中同时运行独立的部分。这个功能叫做线程thread)。

-

将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。

+

将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程仍然需要以与只期望在单个线程中编程不同的方式思考和组织代码。

+

编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。另外,很多编程语言提供了自己的特殊的线程实现。编程语言提供的线程有时被称作轻量级lightweight)或绿色green)线程。这些语言将一系列绿色线程放入不同数量的操作系统线程中执行。因为这个原因,语言调用操作系统 API 创建线程的模型有时被称为 1:1,一个 OS 线程对应一个语言线程。绿色线程模型被称为 M:N 模型,M个绿色线程对应N个 OS 线程,这里MN不必相同。

+

每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。运行时是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时不能在为了维持性能而能够在 C 语言中调用方面做出妥协。

+

绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更多的线程控制和更低的上下文切换消耗。

+

现在我们明白了 Rust 中的线程是如何定义的,让我们开始探索如何使用标准库提供的线程相关的 API吧。

+

使用spawn创建新线程

+

为了创建一个新线程,调用thread::spawn函数并传递一个闭包(第十三章学习了闭包),它包含希望在新线程运行的代码。列表 16-1 中的例子在新线程中打印了一些文本而其余的文本在主线程中打印:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    thread::spawn(|| {
+        for i in 1..10 {
+            println!("hi number {} from the spawned thread!", i);
+        }
+    });
+
+    for i in 1..5 {
+        println!("hi number {} from the main thread!", i);
+    }
+}
+
+

Listing 16-1: Creating a new thread to print one thing +while the main thread is printing something else

+

注意这个函数编写的方式,当主线程结束时,它也会停止新线程。这个程序的输出每次可能都略微不同,不过它大体上看起来像这样:

+
hi number 1 from the main thread!
+hi number 1 from the spawned thread!
+hi number 2 from the main thread!
+hi number 2 from the spawned thread!
+hi number 3 from the main thread!
+hi number 3 from the spawned thread!
+hi number 4 from the main thread!
+hi number 4 from the spawned thread!
+hi number 5 from the spawned thread!
+
+

这些线程可能会轮流运行,不过并不保证如此。在这里,主线程先行打印,即便新创建线程的打印语句位于程序的开头。甚至即便我们告诉新建的线程打印直到i等于 9 ,它在主线程结束之前也只打印到了 5。如果你只看到了一个线程,或没有出现重叠打印的现象,尝试增加 range 的数值来增加线程暂停并切换到其他线程运行的机会。

+

使用join等待所有线程结束

+

由于主线程先于新建线程结束,不仅列表 16-1 中的代码大部分时候不能保证新建线程执行完毕,甚至不能实际保证新建线程会被执行!可以通过保存thread::spawn的返回值来解决这个问题,这是一个JoinHandle。这看起来如列表 16-2 所示:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let handle = thread::spawn(|| {
+        for i in 1..10 {
+            println!("hi number {} from the spawned thread!", i);
+        }
+    });
+
+    for i in 1..5 {
+        println!("hi number {} from the main thread!", i);
+    }
+
+    handle.join();
+}
+
+

Listing 16-2: Saving a JoinHandle from thread::spawn +to guarantee the thread is run to completion

+

JoinHandle是一个拥有所有权的值,它可以等待一个线程结束,这也正是join方法所做的。通过调用这个句柄的join,当前线程会阻塞直到句柄所代表的线程结束。因为我们将join调用放在了主线程的for循环之后,运行这个例子将产生类似这样的输出:

+
hi number 1 from the main thread!
+hi number 2 from the main thread!
+hi number 1 from the spawned thread!
+hi number 3 from the main thread!
+hi number 2 from the spawned thread!
+hi number 4 from the main thread!
+hi number 3 from the spawned thread!
+hi number 4 from the spawned thread!
+hi number 5 from the spawned thread!
+hi number 6 from the spawned thread!
+hi number 7 from the spawned thread!
+hi number 8 from the spawned thread!
+hi number 9 from the spawned thread!
+
+

这两个线程仍然会交替执行,不过主线程会由于handle.join()调用会等待直到新建线程执行完毕。

+

如果将handle.join()放在主线程的for循环之前,像这样:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let handle = thread::spawn(|| {
+        for i in 1..10 {
+            println!("hi number {} from the spawned thread!", i);
+        }
+    });
+
+    handle.join();
+
+    for i in 1..5 {
+        println!("hi number {} from the main thread!", i);
+    }
+}
+
+

主线程会等待直到新建线程执行完毕之后才开始执行for循环,所以输出将不会交替出现:

+
hi number 1 from the spawned thread!
+hi number 2 from the spawned thread!
+hi number 3 from the spawned thread!
+hi number 4 from the spawned thread!
+hi number 5 from the spawned thread!
+hi number 6 from the spawned thread!
+hi number 7 from the spawned thread!
+hi number 8 from the spawned thread!
+hi number 9 from the spawned thread!
+hi number 1 from the main thread!
+hi number 2 from the main thread!
+hi number 3 from the main thread!
+hi number 4 from the main thread!
+
+

稍微考虑一下将join放置与何处会影响线程是否同时运行。

+

线程和move闭包

+

第十三章有一个我们没有讲到的闭包功能,它经常用于thread::spawnmove闭包。第十三章中讲到:

+
+

获取他们环境中值的闭包主要用于开始新线程的场景

+
+

现在我们正在创建新线程,所以让我们讨论一下获取环境值的闭包吧!

+

注意列表 16-1 中传递给thread::spawn的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。列表 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let v = vec![1, 2, 3];
+
+    let handle = thread::spawn(|| {
+        println!("Here's a vector: {:?}", v);
+    });
+
+    handle.join();
+}
+
+

Listing 16-3: Attempting to use a vector created by the +main thread from another thread

+

闭包使用了v,所以闭包会获取v并使其成为闭包环境的一部分。因为thread::spawn在一个新线程中运行这个闭包,所以可以在新线程中访问v

+

然而当编译这个例子时,会得到如下错误:

+
error[E0373]: closure may outlive the current function, but it borrows `v`,
+which is owned by the current function
+ -->
+  |
+6 |     let handle = thread::spawn(|| {
+  |                                ^^ may outlive borrowed value `v`
+7 |         println!("Here's a vector: {:?}", v);
+  |                                           - `v` is borrowed here
+  |
+help: to force the closure to take ownership of `v` (and any other referenced
+variables), use the `move` keyword, as shown:
+  |     let handle = thread::spawn(move || {
+
+

当在闭包环境中获取某些值时,Rust 会尝试推断如何获取它。println!只需要v的一个引用,所以闭包尝试借用v。但是这有一个问题:我们并不知道新建线程会运行多久,所以无法知道v是否一直时有效的。

+

考虑一下列表 16-4 中的代码,它展示了一个v的引用很有可能不再有效的场景:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let v = vec![1, 2, 3];
+
+    let handle = thread::spawn(|| {
+        println!("Here's a vector: {:?}", v);
+    });
+
+    drop(v); // oh no!
+
+    handle.join();
+}
+
+

Listing 16-4: A thread with a closure that attempts to +capture a reference to v from a main thread that drops v

+

这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个v的引用,不过主线程仍在执行:它立刻丢弃了v,使用了第十五章提到的显式丢弃其参数的drop函数。接着,新建线程开始执行,现在v是无效的了,所以它的引用也就是无效得的。噢,这太糟了!

+

为了修复这个问题,我们可以听取错误信息的建议:

+
help: to force the closure to take ownership of `v` (and any other referenced
+variables), use the `move` keyword, as shown:
+  |     let handle = thread::spawn(move || {
+
+

通过在闭包之前增加move关键字,我们强制闭包获取它使用的值的所有权,而不是引用借用。列表 16-5 中展示的对列表 16-3 代码的修改可以按照我们的预期编译并运行:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let v = vec![1, 2, 3];
+
+    let handle = thread::spawn(move || {
+        println!("Here's a vector: {:?}", v);
+    });
+
+    handle.join();
+}
+
+

Listing 16-5: Using the move keyword to force a closure +to take ownership of the values it uses

+

那么列表 16-4 中那个主线程调用了drop的代码该怎么办呢?如果在闭包上增加了move,就将v移动到了闭包的环境中,我们将不能对其调用drop了。相反会出现这个编译时错误:

+
error[E0382]: use of moved value: `v`
+  -->
+   |
+6  |     let handle = thread::spawn(move || {
+   |                                ------- value moved (into closure) here
+...
+10 |     drop(v); // oh no!
+   |          ^ value used here after move
+   |
+   = note: move occurs because `v` has type `std::vec::Vec<i32>`, which does
+   not implement the `Copy` trait
+
+

Rust 的所有权规则又一次帮助了我们!

+

现在我们有一个线程和线程 API 的基本了解,让我们讨论一下使用线程实际可以什么吧。

diff --git a/docs/ch16-02-message-passing.html b/docs/ch16-02-message-passing.html index 9abb018..8be316a 100644 --- a/docs/ch16-02-message-passing.html +++ b/docs/ch16-02-message-passing.html @@ -67,7 +67,213 @@
- +

使用消息传递在线程间传送数据

+
+

ch16-02-message-passing.md +
+commit da15de39eaabd50100d6fa662c653169254d9175

+
+

最近人气正在上升的一个并发方式是消息传递message passing),这里线程或 actor 通过发送包含数据的消息来沟通。这个思想来源于口号:

+
+

Do not communicate by sharing memory; instead, share memory by +communicating.

+

不要共享内存来通讯;而是要通讯来共享内存。

+

--Effective Go

+
+

实现这个目标的主要工具是通道channel)。通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。代码的一部分可以调用发送者和想要发送的数据,而另一部分代码可以在接收的那一端收取消息。

+

我们将编写一个例子使用一个线程生成值并向通道发送他们。主线程会接收这些值并打印出来。

+

首先,如列表 16-6 所示,先创建一个通道但不做任何事:

+

Filename: src/main.rs

+
use std::sync::mpsc;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+#     tx.send(()).unwrap();
+}
+
+

Listing 16-6: Creating a channel and assigning the two +halves to tx and rx

+

mpsc::channel函数创建一个新的通道。mpsc多个生产者,单个消费者multiple producer, single consumer)的缩写。简而言之,可以有多个产生值的发送端,但只能有一个消费这些值的接收端。现在我们以一个单独的生产者开始,不过一旦例子可以工作了就会增加多个生产者。

+

mpsc::channel返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,很多人使用txrx作为发送者接收者的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个let语句和模式来解构了元组。第十八章会讨论let语句中的模式和解构。

+

让我们将发送端移动到一个新建线程中并发送一个字符串,如列表 16-7 所示:

+

Filename: src/main.rs

+
use std::thread;
+use std::sync::mpsc;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+
+    thread::spawn(move || {
+        let val = String::from("hi");
+        tx.send(val).unwrap();
+    });
+}
+
+

Listing 16-7: Moving tx to a spawned thread and sending +"hi"

+

正如上一部分那样使用thread::spawn来创建一个新线程。并使用一个move闭包来将tx移动进闭包这样新建线程就是其所有者。

+

通道的发送端有一个send方法用来获取需要放入通道的值。send方法返回一个Result<T, E>类型,因为如果接收端被丢弃了,将没有发送值的目标,所以发送操作会出错。在这个例子中,我们简单的调用unwrap来忽略错误,不过对于一个真实程序,需要合理的处理它。第九章是你复习正确错误处理策略的好地方。

+

在列表 16-8 中,让我们在主线程中从通道的接收端获取值:

+

Filename: src/main.rs

+
use std::thread;
+use std::sync::mpsc;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+
+    thread::spawn(move || {
+        let val = String::from("hi");
+        tx.send(val).unwrap();
+    });
+
+    let received = rx.recv().unwrap();
+    println!("Got: {}", received);
+}
+
+

Listing 16-8: Receiving the value "hi" in the main thread +and printing it out

+

通道的接收端有两个有用的方法:recvtry_recv。这里,我们使用了recv,它是 receive 的缩写。这个方法会阻塞执行直到从通道中接收一个值。一旦发送了一个值,recv会在一个Result<T, E>中返回它。当通道发送端关闭,recv会返回一个错误。try_recv不会阻塞;相反它立刻返回一个Result<T, E>

+

如果运行列表 16-8 中的代码,我们将会看到主线程打印出这个值:

+
Got: hi
+
+

通道与所有权如何交互

+

现在让我们做一个试验来看看通道与所有权如何在一起工作:我们将尝试在新建线程中的通道中发送完val之后再使用它。尝试编译列表 16-9 中的代码:

+

Filename: src/main.rs

+
use std::thread;
+use std::sync::mpsc;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+
+    thread::spawn(move || {
+        let val = String::from("hi");
+        tx.send(val).unwrap();
+        println!("val is {}", val);
+    });
+
+    let received = rx.recv().unwrap();
+    println!("Got: {}", received);
+}
+
+

Listing 16-9: Attempting to use val after we have sent +it down the channel

+

这里尝试在通过tx.send发送val到通道中之后将其打印出来。这是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们在此使用它之前就修改或者丢弃它。这会由于不一致或不存在的数据而导致错误或意外的结果。

+

尝试编译这些代码,Rust 会报错:

+
error[E0382]: use of moved value: `val`
+  --> src/main.rs:10:31
+   |
+9  |         tx.send(val).unwrap();
+   |                 --- value moved here
+10 |         println!("val is {}", val);
+   |                               ^^^ value used here after move
+   |
+   = note: move occurs because `val` has type `std::string::String`, which does
+   not implement the `Copy` trait
+
+

我们的并发错误会造成一个编译时错误!send获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。

+

在这一点上,消息传递非常类似于 Rust 的单所有权系统。消息传递的拥护者出于相似的原因支持消息传递,就像 Rustacean 们欣赏 Rust 的所有权一样:单所有权意味着特定类型问题的消失。如果一次只有一个线程可以使用某些内存,就没有出现数据竞争的机会。

+

发送多个值并观察接收者的等待

+

列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂定一段时间。

+

Filename: src/main.rs

+
use std::thread;
+use std::sync::mpsc;
+use std::time::Duration;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+
+    thread::spawn(move || {
+        let vals = vec![
+            String::from("hi"),
+            String::from("from"),
+            String::from("the"),
+            String::from("thread"),
+        ];
+
+        for val in vals {
+            tx.send(val).unwrap();
+            thread::sleep(Duration::new(1, 0));
+        }
+    });
+
+    for received in rx {
+        println!("Got: {}", received);
+    }
+}
+
+

Listing 16-10: Sending multiple messages and pausing +between each one

+

这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个Duration值调用thread::sleep函数来暂停一秒。

+

在主线程中,不再显式的调用recv函数:而是将rx当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。

+

当运行列表 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒:

+
Got: hi
+Got: from
+Got: the
+Got: thread
+
+

在主线程中并没有任何暂停或位于for循环中用于等待的代码,所以可以说主线程是在等待从新建线程中接收值。

+

通过克隆发送者来创建多个生产者

+

差不多在本部分的开头,我们提到了mpscmultiple producer, single consumer 的缩写。可以扩展列表 16-11 中的代码来创建都向同一接收者发送值的多个线程。这可以通过克隆通道的发送端在来做到,如列表 16-11 所示:

+

Filename: src/main.rs

+
# use std::thread;
+# use std::sync::mpsc;
+# use std::time::Duration;
+#
+# fn main() {
+// ...snip...
+let (tx, rx) = mpsc::channel();
+
+let tx1 = tx.clone();
+thread::spawn(move || {
+    let vals = vec![
+        String::from("hi"),
+        String::from("from"),
+        String::from("the"),
+        String::from("thread"),
+    ];
+
+    for val in vals {
+        tx1.send(val).unwrap();
+        thread::sleep(Duration::new(1, 0));
+    }
+});
+
+thread::spawn(move || {
+    let vals = vec![
+        String::from("more"),
+        String::from("messages"),
+        String::from("for"),
+        String::from("you"),
+    ];
+
+    for val in vals {
+        tx.send(val).unwrap();
+        thread::sleep(Duration::new(1, 0));
+    }
+});
+// ...snip...
+#
+#     for received in rx {
+#         println!("Got: {}", received);
+#     }
+# }
+
+

Listing 16-11: Sending multiple messages and pausing +between each one

+

这一次,在创建新线程之前,我们对通道的发送端调用了clone方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程,这样每个线程将向通道的接收端发送不同的消息。

+

如果运行这些代码,你可能会看到这样的输出:

+
Got: hi
+Got: more
+Got: from
+Got: messages
+Got: for
+Got: the
+Got: thread
+Got: you
+
+

虽然你可能会看到这些以不同的顺序出现。这依赖于你的系统!这也就是并发既有趣又困难的原因。如果你拿thread::sleep做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定并每次都会产生不同的输出。

+

现在我们见识过了通道如何工作,再看看共享内存并发吧。

+
diff --git a/docs/print.html b/docs/print.html index e118f70..d591de3 100644 --- a/docs/print.html +++ b/docs/print.html @@ -1034,7 +1034,7 @@ commit 04aa3a45eb72855b34213703718f50a12a3eeec8

复合类型

复合类型可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

将值组合进元组

-

元组是一个将多个其他类型的值组合进一个复合类型的组要方式。

+

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。

我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这写不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:

Filename: src/main.rs

fn main() {
@@ -2233,12 +2233,11 @@ let slice = &a[1..3];
 

ch05-00-structs.md
-commit 255b44b409585e472e14c396ebc75d28f540a1ac

+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

-

struct,是 structure 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,struct就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定行为的函数。structenum(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查,来在程序范围创建新类型的基本组件。

-

对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命令各部分数据所以能更清楚的知道其值是什么意思。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。

-

为了定义结构体,通过struct关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作字段fields),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体:

-
+

struct,是 structure 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,struct就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定行为的函数。structenum(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查来在程序范围内创建新类型的基本组件。

+

对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命名各部分数据以便能更清楚的知道其值的意义。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。

+

为了定义结构体,通过struct关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作字段field),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体:

struct User {
     username: String,
     email: String,
@@ -2246,11 +2245,8 @@ commit 255b44b409585e472e14c396ebc75d28f540a1ac

active: bool, }
-
-

Listing 5-1: A User struct definition

-
-
-

一旦定义后为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用key: value对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板。例如,我们可以像这样来声明一个特定的用户:

+

Listing 5-1: A User struct definition

+

一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用key: value对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,我们可以像这样来声明一个特定的用户:

# struct User {
 #     username: String,
 #     email: String,
@@ -2303,8 +2299,7 @@ error[E0106]: missing lifetime specifier
 

一个示例程序

为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。

使用 Cargo 来创建一个叫做 rectangles 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 src/main.rs 文件中为此实现的一个小程序:

-
-Filename: src/main.rs +

Filename: src/main.rs

fn main() {
     let length1 = 50;
     let width1 = 30;
@@ -2319,23 +2314,19 @@ fn area(length: u32, width: u32) -> u32 {
     length * width
 }
 
-
-

Listing 5-2: Calculating the area of a rectangle specified by its length and -width in separate variables

-
-
+

Listing 5-2: Calculating the area of a rectangle +specified by its length and width in separate variables

尝试使用cargo run运行程序:

The area of the rectangle is 1500 square pixels.
 

使用元组重构

-

我们的小程序能正常运行;它调用area函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们一起才能定义一个长方形。

+

我们的小程序能正常运行;它调用area函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们在一起才能定义一个长方形。

这个做法的问题突显在area的签名上:

fn area(length: u32, width: u32) -> u32 {
 

函数area本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。

第三章已经讨论过了一种可行的方法:元组。列表 5-3 是一个使用元组的版本:

-
-Filename: src/main.rs +

Filename: src/main.rs

fn main() {
     let rect1 = (50, 30);
 
@@ -2349,21 +2340,18 @@ fn area(dimensions: (u32, u32)) -> u32 {
     dimensions.0 * dimensions.1
 }
 
-
-

Listing 5-3: Specifying the length and width of the rectangle with a tuple

-
-
+

Listing 5-3: Specifying the length and width of the +rectangle with a tuple

-

在某种程度上说这样好一点了。元组帮助我们增加了一些结构,现在在调用area的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:

+

在某种程度上说这样好一点了。元组帮助我们增加了一些结构性,现在在调用area的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:

dimensions.0 * dimensions.1
 
-

在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引0length1width。如果其他人要使用这些代码,他们也不得不搞清楚后再记住。容易忘记或者混淆这些值而造成错误,因为我们没有表达我们代码中数据的意义。

+

在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引0length1width。如果其他人要使用这些代码,他们也不得不搞清楚后再记住他们。容易忘记或者混淆这些值而造成错误,因为我们没有表明代码中数据的意义。

使用结构体重构:增加更多意义

-

现在引入结构体。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示:

-
-Filename: src/main.rs +

现在引入结构体的时候了。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示:

+

Filename: src/main.rs

struct Rectangle {
     length: u32,
     width: u32,
@@ -2382,18 +2370,14 @@ fn area(rectangle: &Rectangle) -> u32 {
     rectangle.length * rectangle.width
 }
 
-
-

Listing 5-4: Defining a Rectangle struct

-
-
+

Listing 5-4: Defining a Rectangle struct

这里我们定义了一个结构体并称其为Rectangle。在{}中定义了字段lengthwidth,都是u32类型的。接着在main中,我们创建了一个长度为 50 和宽度为 30 的Rectangle的具体实例。

函数area现在被定义为接收一个名叫rectangle的参数,它的类型是一个结构体Rectangle实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样main函数就可以保持rect1的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有&

-

area函数访问Rectanglelengthwidth字段。area的签名现在明确的表明了我们的意图:计算一个Rectangle的面积,通过其lengthwidth字段。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值01。这是明确性的胜利。

+

area函数访问Rectanglelengthwidth字段。area的签名现在明确的表明了我们的意图:通过其lengthwidth字段,计算一个Rectangle的面积,。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值01。这是明确性的胜利。

通过衍生 trait 增加实用功能

如果能够在调试程序时打印出Rectangle实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用println!宏:

-
-Filename: src/main.rs +

Filename: src/main.rs

struct Rectangle {
     length: u32,
     width: u32,
@@ -2405,10 +2389,8 @@ fn main() {
     println!("rect1 is {}", rect1);
 }
 
-
-

Listing 5-5: Attempting to print a Rectangle instance

-
-
+

Listing 5-5: Attempting to print a Rectangle +instance

如果运行代码,会出现带有如下核心信息的错误:

error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
 
@@ -2426,7 +2408,6 @@ fn main() { crate, add `#[derive(Debug)]` or manually implement it

Rust 确实包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上#[derive(Debug)]注解,如列表 5-6 所示:

-
#[derive(Debug)]
 struct Rectangle {
     length: u32,
@@ -2439,11 +2420,8 @@ fn main() {
     println!("rect1 is {:?}", rect1);
 }
 
-
-

Listing 5-6: Adding the annotation to derive the Debug trait and printing the -Rectangle instance using debug formatting

-
-
+

Listing 5-6: Adding the annotation to derive the Debug +trait and printing the Rectangle instance using debug formatting

此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出:

rect1 is Rectangle { length: 50, width: 30 }
 
@@ -2459,13 +2437,12 @@ fn main() {

ch05-01-method-syntax.md
-commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

+commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2

-

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十三章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

+

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

定义方法

让我们将获取一个Rectangle实例作为参数的area函数改写成一个定义于Rectangle结构体上的area方法,如列表 5-7 所示:

-
-Filename: src/main.rs +

Filename: src/main.rs

#[derive(Debug)]
 struct Rectangle {
     length: u32,
@@ -2487,15 +2464,13 @@ fn main() {
     );
 }
 
-
-

Listing 5-7: Defining an area method on the Rectangle struct

-
-
+

Listing 5-7: Defining an area method on the Rectangle +struct

为了使函数定义于Rectangle的上下文中,我们开始了一个impl块(implimplementation 的缩写)。接着将函数移动到impl大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成self。然后在main中将我们调用area方法并传递rect1作为参数的地方,改成使用方法语法Rectangle实例上调用area方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。

area的签名中,开始使用&self来替代rectangle: &Rectangle,因为该方法位于impl Rectangle 上下文中所以 Rust 知道self的类型是Rectangle。注意仍然需要在self前面加上&,就像&Rectangle一样。方法可以选择获取self的所有权,像我们这里一样不可变的借用self,或者可变的借用self,就跟其他别的参数一样。

这里选择&self跟在函数版本中使用&Rectangle出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将抵押给参数改为&mut self。通过仅仅使用self作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。

-

使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl块中,而不是让将来的用户在我们的代码中到处寻找`Rectangle的功能。

+

使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl块中,而不是让将来的用户在我们的代码中到处寻找Rectangle的功能。

->运算符到哪去了?

@@ -2526,8 +2501,7 @@ p1.distance(&p2);

带有更多参数的方法

让我们更多的实践一下方法,通过为Rectangle结构体实现第二个方法。这回,我们让一个Rectangle的实例获取另一个Rectangle实例并返回self能否完全包含第二个长方形,如果能返回true若不能则返回false。当我们定义了can_hold方法,就可以运行列表 5-8 中的代码了:

-
-Filename: src/main.rs +

Filename: src/main.rs

fn main() {
     let rect1 = Rectangle { length: 50, width: 30 };
     let rect2 = Rectangle { length: 40, width: 10 };
@@ -2537,15 +2511,13 @@ p1.distance(&p2);
     println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
 }
 
-
-

Listing 5-8: Demonstration of using the as-yet-unwritten can_hold method

-
-
+

Listing 5-8: Demonstration of using the as-yet-unwritten +can_hold method

我们希望看到如下输出,因为rect2的长宽都小于rect1,而rect3rect1要宽:

Can rect1 hold rect2? true
 Can rect1 hold rect3? false
 
-

因为我们想定义一个方法,所以它应该位于impl Rectangle块中。方法名是can_hold,并且它会获取另一个Rectangle的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:rect1.can_hold(&rect2)传入了&rect2,它是一个Rectangle的实例rect2的不可变借用。这是可以理解的,因为我们只需要读取rect2(而不是写入,这意味着我们需要一个可变借用)而且希望main保持rect2的所有权这样就可以在调用这个方法后继续使用它。can_hold的返回值是一个布尔值,其实现会分别检查self的长宽是够都大于另一个Rectangle。让我们在列表 5-7 的impl块中增加这个新方法:

+

因为我们想定义一个方法,所以它应该位于impl Rectangle块中。方法名是can_hold,并且它会获取另一个Rectangle的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:rect1.can_hold(&rect2)传入了&rect2,它是一个Rectangle的实例rect2的不可变借用。这是可以理解的,因为我们只需要读取rect2(而不是写入,这意味着我们需要一个可变借用)而且希望main保持rect2的所有权这样就可以在调用这个方法后继续使用它。can_hold的返回值是一个布尔值,其实现会分别检查self的长宽是够都大于另一个Rectangle。让我们在列表 5-7 的impl块中增加这个新方法,如列表 5-9 所示:

Filename: src/main.rs

# #[derive(Debug)]
 # struct Rectangle {
@@ -2563,6 +2535,8 @@ impl Rectangle {
     }
 }
 
+

Listing 5-9: Implementing the can_hold method on +Rectangle that takes another Rectangle instance as an argument

如果结合列表 5-8 的main函数来运行,就会看到想要得到的输出!方法可以在self后增加多个参数,而且这些参数就像函数中的参数一样工作。

关联函数

@@ -2589,7 +2563,7 @@ impl Rectangle {

ch06-00-enums.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+commit 4f2dc564851dc04b271a2260c834643dfd86c724

本章介绍枚举,也被称作 enums。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做Option,它代表一个值要么是一些值要么什么都不是。然后会讲到match表达式中的模式匹配如何使对枚举不同的值运行不同的代码变得容易。最后会涉及到if let,另一个简洁方便处理代码中枚举的结构。

枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像F#、OCaml 和 Haskell这样的函数式编程语言中的代数数据类型algebraic data types)最为相似。

@@ -2597,7 +2571,7 @@ commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

ch06-01-defining-an-enum.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+commit e6d6caab41471f7115a621029bd428a812c5260e

让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以枚举出所有可能的值,这也正是它名字的由来。

任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值尽可能是其一个成员。IPv4 和 IPv6 从根本上讲都是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。

@@ -8827,7 +8801,412 @@ commit 55b294f20fc846a13a9be623bf322d8b364cee77

在今天使用的大部分操作系统中,当程序执行时,操作系统运行代码的上下文称为进程process)。操作系统可以运行很多进程,而操作系统也管理这些进程使得多个程序可以在电脑上同时运行。

我们可以将每个进程运行一个程序的概念再往下抽象一层:程序也可以在其上下文中同时运行独立的部分。这个功能叫做线程thread)。

-

将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。

+

将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程仍然需要以与只期望在单个线程中编程不同的方式思考和组织代码。

+

编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。另外,很多编程语言提供了自己的特殊的线程实现。编程语言提供的线程有时被称作轻量级lightweight)或绿色green)线程。这些语言将一系列绿色线程放入不同数量的操作系统线程中执行。因为这个原因,语言调用操作系统 API 创建线程的模型有时被称为 1:1,一个 OS 线程对应一个语言线程。绿色线程模型被称为 M:N 模型,M个绿色线程对应N个 OS 线程,这里MN不必相同。

+

每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。运行时是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时不能在为了维持性能而能够在 C 语言中调用方面做出妥协。

+

绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更多的线程控制和更低的上下文切换消耗。

+

现在我们明白了 Rust 中的线程是如何定义的,让我们开始探索如何使用标准库提供的线程相关的 API吧。

+

使用spawn创建新线程

+

为了创建一个新线程,调用thread::spawn函数并传递一个闭包(第十三章学习了闭包),它包含希望在新线程运行的代码。列表 16-1 中的例子在新线程中打印了一些文本而其余的文本在主线程中打印:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    thread::spawn(|| {
+        for i in 1..10 {
+            println!("hi number {} from the spawned thread!", i);
+        }
+    });
+
+    for i in 1..5 {
+        println!("hi number {} from the main thread!", i);
+    }
+}
+
+

Listing 16-1: Creating a new thread to print one thing +while the main thread is printing something else

+

注意这个函数编写的方式,当主线程结束时,它也会停止新线程。这个程序的输出每次可能都略微不同,不过它大体上看起来像这样:

+
hi number 1 from the main thread!
+hi number 1 from the spawned thread!
+hi number 2 from the main thread!
+hi number 2 from the spawned thread!
+hi number 3 from the main thread!
+hi number 3 from the spawned thread!
+hi number 4 from the main thread!
+hi number 4 from the spawned thread!
+hi number 5 from the spawned thread!
+
+

这些线程可能会轮流运行,不过并不保证如此。在这里,主线程先行打印,即便新创建线程的打印语句位于程序的开头。甚至即便我们告诉新建的线程打印直到i等于 9 ,它在主线程结束之前也只打印到了 5。如果你只看到了一个线程,或没有出现重叠打印的现象,尝试增加 range 的数值来增加线程暂停并切换到其他线程运行的机会。

+

使用join等待所有线程结束

+

由于主线程先于新建线程结束,不仅列表 16-1 中的代码大部分时候不能保证新建线程执行完毕,甚至不能实际保证新建线程会被执行!可以通过保存thread::spawn的返回值来解决这个问题,这是一个JoinHandle。这看起来如列表 16-2 所示:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let handle = thread::spawn(|| {
+        for i in 1..10 {
+            println!("hi number {} from the spawned thread!", i);
+        }
+    });
+
+    for i in 1..5 {
+        println!("hi number {} from the main thread!", i);
+    }
+
+    handle.join();
+}
+
+

Listing 16-2: Saving a JoinHandle from thread::spawn +to guarantee the thread is run to completion

+

JoinHandle是一个拥有所有权的值,它可以等待一个线程结束,这也正是join方法所做的。通过调用这个句柄的join,当前线程会阻塞直到句柄所代表的线程结束。因为我们将join调用放在了主线程的for循环之后,运行这个例子将产生类似这样的输出:

+
hi number 1 from the main thread!
+hi number 2 from the main thread!
+hi number 1 from the spawned thread!
+hi number 3 from the main thread!
+hi number 2 from the spawned thread!
+hi number 4 from the main thread!
+hi number 3 from the spawned thread!
+hi number 4 from the spawned thread!
+hi number 5 from the spawned thread!
+hi number 6 from the spawned thread!
+hi number 7 from the spawned thread!
+hi number 8 from the spawned thread!
+hi number 9 from the spawned thread!
+
+

这两个线程仍然会交替执行,不过主线程会由于handle.join()调用会等待直到新建线程执行完毕。

+

如果将handle.join()放在主线程的for循环之前,像这样:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let handle = thread::spawn(|| {
+        for i in 1..10 {
+            println!("hi number {} from the spawned thread!", i);
+        }
+    });
+
+    handle.join();
+
+    for i in 1..5 {
+        println!("hi number {} from the main thread!", i);
+    }
+}
+
+

主线程会等待直到新建线程执行完毕之后才开始执行for循环,所以输出将不会交替出现:

+
hi number 1 from the spawned thread!
+hi number 2 from the spawned thread!
+hi number 3 from the spawned thread!
+hi number 4 from the spawned thread!
+hi number 5 from the spawned thread!
+hi number 6 from the spawned thread!
+hi number 7 from the spawned thread!
+hi number 8 from the spawned thread!
+hi number 9 from the spawned thread!
+hi number 1 from the main thread!
+hi number 2 from the main thread!
+hi number 3 from the main thread!
+hi number 4 from the main thread!
+
+

稍微考虑一下将join放置与何处会影响线程是否同时运行。

+

线程和move闭包

+

第十三章有一个我们没有讲到的闭包功能,它经常用于thread::spawnmove闭包。第十三章中讲到:

+
+

获取他们环境中值的闭包主要用于开始新线程的场景

+
+

现在我们正在创建新线程,所以让我们讨论一下获取环境值的闭包吧!

+

注意列表 16-1 中传递给thread::spawn的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。列表 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let v = vec![1, 2, 3];
+
+    let handle = thread::spawn(|| {
+        println!("Here's a vector: {:?}", v);
+    });
+
+    handle.join();
+}
+
+

Listing 16-3: Attempting to use a vector created by the +main thread from another thread

+

闭包使用了v,所以闭包会获取v并使其成为闭包环境的一部分。因为thread::spawn在一个新线程中运行这个闭包,所以可以在新线程中访问v

+

然而当编译这个例子时,会得到如下错误:

+
error[E0373]: closure may outlive the current function, but it borrows `v`,
+which is owned by the current function
+ -->
+  |
+6 |     let handle = thread::spawn(|| {
+  |                                ^^ may outlive borrowed value `v`
+7 |         println!("Here's a vector: {:?}", v);
+  |                                           - `v` is borrowed here
+  |
+help: to force the closure to take ownership of `v` (and any other referenced
+variables), use the `move` keyword, as shown:
+  |     let handle = thread::spawn(move || {
+
+

当在闭包环境中获取某些值时,Rust 会尝试推断如何获取它。println!只需要v的一个引用,所以闭包尝试借用v。但是这有一个问题:我们并不知道新建线程会运行多久,所以无法知道v是否一直时有效的。

+

考虑一下列表 16-4 中的代码,它展示了一个v的引用很有可能不再有效的场景:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let v = vec![1, 2, 3];
+
+    let handle = thread::spawn(|| {
+        println!("Here's a vector: {:?}", v);
+    });
+
+    drop(v); // oh no!
+
+    handle.join();
+}
+
+

Listing 16-4: A thread with a closure that attempts to +capture a reference to v from a main thread that drops v

+

这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个v的引用,不过主线程仍在执行:它立刻丢弃了v,使用了第十五章提到的显式丢弃其参数的drop函数。接着,新建线程开始执行,现在v是无效的了,所以它的引用也就是无效得的。噢,这太糟了!

+

为了修复这个问题,我们可以听取错误信息的建议:

+
help: to force the closure to take ownership of `v` (and any other referenced
+variables), use the `move` keyword, as shown:
+  |     let handle = thread::spawn(move || {
+
+

通过在闭包之前增加move关键字,我们强制闭包获取它使用的值的所有权,而不是引用借用。列表 16-5 中展示的对列表 16-3 代码的修改可以按照我们的预期编译并运行:

+

Filename: src/main.rs

+
use std::thread;
+
+fn main() {
+    let v = vec![1, 2, 3];
+
+    let handle = thread::spawn(move || {
+        println!("Here's a vector: {:?}", v);
+    });
+
+    handle.join();
+}
+
+

Listing 16-5: Using the move keyword to force a closure +to take ownership of the values it uses

+

那么列表 16-4 中那个主线程调用了drop的代码该怎么办呢?如果在闭包上增加了move,就将v移动到了闭包的环境中,我们将不能对其调用drop了。相反会出现这个编译时错误:

+
error[E0382]: use of moved value: `v`
+  -->
+   |
+6  |     let handle = thread::spawn(move || {
+   |                                ------- value moved (into closure) here
+...
+10 |     drop(v); // oh no!
+   |          ^ value used here after move
+   |
+   = note: move occurs because `v` has type `std::vec::Vec<i32>`, which does
+   not implement the `Copy` trait
+
+

Rust 的所有权规则又一次帮助了我们!

+

现在我们有一个线程和线程 API 的基本了解,让我们讨论一下使用线程实际可以什么吧。

+

使用消息传递在线程间传送数据

+
+

ch16-02-message-passing.md +
+commit da15de39eaabd50100d6fa662c653169254d9175

+
+

最近人气正在上升的一个并发方式是消息传递message passing),这里线程或 actor 通过发送包含数据的消息来沟通。这个思想来源于口号:

+
+

Do not communicate by sharing memory; instead, share memory by +communicating.

+

不要共享内存来通讯;而是要通讯来共享内存。

+

--Effective Go

+
+

实现这个目标的主要工具是通道channel)。通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。代码的一部分可以调用发送者和想要发送的数据,而另一部分代码可以在接收的那一端收取消息。

+

我们将编写一个例子使用一个线程生成值并向通道发送他们。主线程会接收这些值并打印出来。

+

首先,如列表 16-6 所示,先创建一个通道但不做任何事:

+

Filename: src/main.rs

+
use std::sync::mpsc;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+#     tx.send(()).unwrap();
+}
+
+

Listing 16-6: Creating a channel and assigning the two +halves to tx and rx

+

mpsc::channel函数创建一个新的通道。mpsc多个生产者,单个消费者multiple producer, single consumer)的缩写。简而言之,可以有多个产生值的发送端,但只能有一个消费这些值的接收端。现在我们以一个单独的生产者开始,不过一旦例子可以工作了就会增加多个生产者。

+

mpsc::channel返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,很多人使用txrx作为发送者接收者的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个let语句和模式来解构了元组。第十八章会讨论let语句中的模式和解构。

+

让我们将发送端移动到一个新建线程中并发送一个字符串,如列表 16-7 所示:

+

Filename: src/main.rs

+
use std::thread;
+use std::sync::mpsc;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+
+    thread::spawn(move || {
+        let val = String::from("hi");
+        tx.send(val).unwrap();
+    });
+}
+
+

Listing 16-7: Moving tx to a spawned thread and sending +"hi"

+

正如上一部分那样使用thread::spawn来创建一个新线程。并使用一个move闭包来将tx移动进闭包这样新建线程就是其所有者。

+

通道的发送端有一个send方法用来获取需要放入通道的值。send方法返回一个Result<T, E>类型,因为如果接收端被丢弃了,将没有发送值的目标,所以发送操作会出错。在这个例子中,我们简单的调用unwrap来忽略错误,不过对于一个真实程序,需要合理的处理它。第九章是你复习正确错误处理策略的好地方。

+

在列表 16-8 中,让我们在主线程中从通道的接收端获取值:

+

Filename: src/main.rs

+
use std::thread;
+use std::sync::mpsc;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+
+    thread::spawn(move || {
+        let val = String::from("hi");
+        tx.send(val).unwrap();
+    });
+
+    let received = rx.recv().unwrap();
+    println!("Got: {}", received);
+}
+
+

Listing 16-8: Receiving the value "hi" in the main thread +and printing it out

+

通道的接收端有两个有用的方法:recvtry_recv。这里,我们使用了recv,它是 receive 的缩写。这个方法会阻塞执行直到从通道中接收一个值。一旦发送了一个值,recv会在一个Result<T, E>中返回它。当通道发送端关闭,recv会返回一个错误。try_recv不会阻塞;相反它立刻返回一个Result<T, E>

+

如果运行列表 16-8 中的代码,我们将会看到主线程打印出这个值:

+
Got: hi
+
+

通道与所有权如何交互

+

现在让我们做一个试验来看看通道与所有权如何在一起工作:我们将尝试在新建线程中的通道中发送完val之后再使用它。尝试编译列表 16-9 中的代码:

+

Filename: src/main.rs

+
use std::thread;
+use std::sync::mpsc;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+
+    thread::spawn(move || {
+        let val = String::from("hi");
+        tx.send(val).unwrap();
+        println!("val is {}", val);
+    });
+
+    let received = rx.recv().unwrap();
+    println!("Got: {}", received);
+}
+
+

Listing 16-9: Attempting to use val after we have sent +it down the channel

+

这里尝试在通过tx.send发送val到通道中之后将其打印出来。这是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们在此使用它之前就修改或者丢弃它。这会由于不一致或不存在的数据而导致错误或意外的结果。

+

尝试编译这些代码,Rust 会报错:

+
error[E0382]: use of moved value: `val`
+  --> src/main.rs:10:31
+   |
+9  |         tx.send(val).unwrap();
+   |                 --- value moved here
+10 |         println!("val is {}", val);
+   |                               ^^^ value used here after move
+   |
+   = note: move occurs because `val` has type `std::string::String`, which does
+   not implement the `Copy` trait
+
+

我们的并发错误会造成一个编译时错误!send获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。

+

在这一点上,消息传递非常类似于 Rust 的单所有权系统。消息传递的拥护者出于相似的原因支持消息传递,就像 Rustacean 们欣赏 Rust 的所有权一样:单所有权意味着特定类型问题的消失。如果一次只有一个线程可以使用某些内存,就没有出现数据竞争的机会。

+

发送多个值并观察接收者的等待

+

列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂定一段时间。

+

Filename: src/main.rs

+
use std::thread;
+use std::sync::mpsc;
+use std::time::Duration;
+
+fn main() {
+    let (tx, rx) = mpsc::channel();
+
+    thread::spawn(move || {
+        let vals = vec![
+            String::from("hi"),
+            String::from("from"),
+            String::from("the"),
+            String::from("thread"),
+        ];
+
+        for val in vals {
+            tx.send(val).unwrap();
+            thread::sleep(Duration::new(1, 0));
+        }
+    });
+
+    for received in rx {
+        println!("Got: {}", received);
+    }
+}
+
+

Listing 16-10: Sending multiple messages and pausing +between each one

+

这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个Duration值调用thread::sleep函数来暂停一秒。

+

在主线程中,不再显式的调用recv函数:而是将rx当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。

+

当运行列表 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒:

+
Got: hi
+Got: from
+Got: the
+Got: thread
+
+

在主线程中并没有任何暂停或位于for循环中用于等待的代码,所以可以说主线程是在等待从新建线程中接收值。

+

通过克隆发送者来创建多个生产者

+

差不多在本部分的开头,我们提到了mpscmultiple producer, single consumer 的缩写。可以扩展列表 16-11 中的代码来创建都向同一接收者发送值的多个线程。这可以通过克隆通道的发送端在来做到,如列表 16-11 所示:

+

Filename: src/main.rs

+
# use std::thread;
+# use std::sync::mpsc;
+# use std::time::Duration;
+#
+# fn main() {
+// ...snip...
+let (tx, rx) = mpsc::channel();
+
+let tx1 = tx.clone();
+thread::spawn(move || {
+    let vals = vec![
+        String::from("hi"),
+        String::from("from"),
+        String::from("the"),
+        String::from("thread"),
+    ];
+
+    for val in vals {
+        tx1.send(val).unwrap();
+        thread::sleep(Duration::new(1, 0));
+    }
+});
+
+thread::spawn(move || {
+    let vals = vec![
+        String::from("more"),
+        String::from("messages"),
+        String::from("for"),
+        String::from("you"),
+    ];
+
+    for val in vals {
+        tx.send(val).unwrap();
+        thread::sleep(Duration::new(1, 0));
+    }
+});
+// ...snip...
+#
+#     for received in rx {
+#         println!("Got: {}", received);
+#     }
+# }
+
+

Listing 16-11: Sending multiple messages and pausing +between each one

+

这一次,在创建新线程之前,我们对通道的发送端调用了clone方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程,这样每个线程将向通道的接收端发送不同的消息。

+

如果运行这些代码,你可能会看到这样的输出:

+
Got: hi
+Got: more
+Got: from
+Got: messages
+Got: for
+Got: the
+Got: thread
+Got: you
+
+

虽然你可能会看到这些以不同的顺序出现。这依赖于你的系统!这也就是并发既有趣又困难的原因。如果你拿thread::sleep做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定并每次都会产生不同的输出。

+

现在我们见识过了通道如何工作,再看看共享内存并发吧。

diff --git a/src/PREFACE.md b/src/PREFACE.md index 12a3ee9..4631943 100644 --- a/src/PREFACE.md +++ b/src/PREFACE.md @@ -1,5 +1,5 @@ # Rust 程序设计语言(第二版) 简体中文版 -还在施工中:正在翻译第十六章 +还在施工中:正在翻译第十六章第二部分 -目前正在解决代码排版问题:已检查到第四章 \ No newline at end of file +目前正在解决代码排版问题:已检查到第五章 \ No newline at end of file diff --git a/src/ch05-00-structs.md b/src/ch05-00-structs.md index 102b031..407acef 100644 --- a/src/ch05-00-structs.md +++ b/src/ch05-00-structs.md @@ -2,15 +2,13 @@ > [ch05-00-structs.md](https://github.com/rust-lang/book/blob/master/src/ch05-00-structs.md) >
-> commit 255b44b409585e472e14c396ebc75d28f540a1ac +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 -`struct`,是 *structure* 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,`struct`就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定**行为**的函数。`struct`和`enum`(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查,来在程序范围创建新类型的基本组件。 +`struct`,是 *structure* 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,`struct`就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定**行为**的函数。`struct`和`enum`(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查来在程序范围内创建新类型的基本组件。 -对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命令各部分数据所以能更清楚的知道其值是什么意思。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。 +对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命名各部分数据以便能更清楚的知道其值的意义。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。 -为了定义结构体,通过`struct`关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作**字段**(*fields*),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体: - -
+为了定义结构体,通过`struct`关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作**字段**(*field*),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体: ```rust struct User { @@ -21,14 +19,9 @@ struct User { } ``` -
+Listing 5-1: A `User` struct definition -Listing 5-1: A `User` struct definition - -
-
- -一旦定义后为了使用它,通过为每个字段指定具体值来创建这个结构体的**实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用`key: value`对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板。例如,我们可以像这样来声明一个特定的用户: +一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的**实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用`key: value`对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,我们可以像这样来声明一个特定的用户: ```rust # struct User { @@ -98,7 +91,6 @@ error[E0106]: missing lifetime specifier 使用 Cargo 来创建一个叫做 *rectangles* 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 *src/main.rs* 文件中为此实现的一个小程序: -
Filename: src/main.rs ```rust @@ -117,13 +109,8 @@ fn area(length: u32, width: u32) -> u32 { } ``` -
- -Listing 5-2: Calculating the area of a rectangle specified by its length and -width in separate variables - -
-
+Listing 5-2: Calculating the area of a rectangle +specified by its length and width in separate variables 尝试使用`cargo run`运行程序: @@ -133,7 +120,7 @@ The area of the rectangle is 1500 square pixels. ### 使用元组重构 -我们的小程序能正常运行;它调用`area`函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们一起才能定义一个长方形。 +我们的小程序能正常运行;它调用`area`函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们在一起才能定义一个长方形。 这个做法的问题突显在`area`的签名上: @@ -145,7 +132,6 @@ fn area(length: u32, width: u32) -> u32 { 第三章已经讨论过了一种可行的方法:元组。列表 5-3 是一个使用元组的版本: -
Filename: src/main.rs ```rust @@ -163,16 +149,12 @@ fn area(dimensions: (u32, u32)) -> u32 { } ``` -
- -Listing 5-3: Specifying the length and width of the rectangle with a tuple - -
-
+Listing 5-3: Specifying the length and width of the +rectangle with a tuple -在某种程度上说这样好一点了。元组帮助我们增加了一些结构,现在在调用`area`的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分: +在某种程度上说这样好一点了。元组帮助我们增加了一些结构性,现在在调用`area`的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分: @@ -181,14 +163,12 @@ we're in libreoffice /Carol --> dimensions.0 * dimensions.1 ``` -在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引`0`是`length`而`1`是`width`。如果其他人要使用这些代码,他们也不得不搞清楚后再记住。容易忘记或者混淆这些值而造成错误,因为我们没有表达我们代码中数据的意义。 +在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引`0`是`length`而`1`是`width`。如果其他人要使用这些代码,他们也不得不搞清楚后再记住他们。容易忘记或者混淆这些值而造成错误,因为我们没有表明代码中数据的意义。 ### 使用结构体重构:增加更多意义 -现在引入结构体。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示: +现在引入结构体的时候了。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示: - -
Filename: src/main.rs ```rust @@ -211,12 +191,7 @@ fn area(rectangle: &Rectangle) -> u32 { } ``` -
- -Listing 5-4: Defining a `Rectangle` struct - -
-
+Listing 5-4: Defining a `Rectangle` struct @@ -224,13 +199,12 @@ Listing 5-4: Defining a `Rectangle` struct 函数`area`现在被定义为接收一个名叫`rectangle`的参数,它的类型是一个结构体`Rectangle`实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样`main`函数就可以保持`rect1`的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有`&`。 -`area`函数访问`Rectangle`的`length`和`width`字段。`area`的签名现在明确的表明了我们的意图:计算一个`Rectangle`的面积,通过其`length`和`width`字段。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值`0`和`1`。这是明确性的胜利。 +`area`函数访问`Rectangle`的`length`和`width`字段。`area`的签名现在明确的表明了我们的意图:通过其`length`和`width`字段,计算一个`Rectangle`的面积,。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值`0`和`1`。这是明确性的胜利。 ### 通过衍生 trait 增加实用功能 如果能够在调试程序时打印出`Rectangle`实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用`println!`宏: -
Filename: src/main.rs ```rust,ignore @@ -246,12 +220,8 @@ fn main() { } ``` -
- -Listing 5-5: Attempting to print a `Rectangle` instance - -
-
+Listing 5-5: Attempting to print a `Rectangle` +instance 如果运行代码,会出现带有如下核心信息的错误: @@ -285,8 +255,6 @@ crate, add `#[derive(Debug)]` or manually implement it Rust **确实**包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上`#[derive(Debug)]`注解,如列表 5-6 所示: -
- ```rust #[derive(Debug)] struct Rectangle { @@ -301,13 +269,8 @@ fn main() { } ``` -
- -Listing 5-6: Adding the annotation to derive the `Debug` trait and printing the -`Rectangle` instance using debug formatting - -
-
+Listing 5-6: Adding the annotation to derive the `Debug` +trait and printing the `Rectangle` instance using debug formatting 此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出: diff --git a/src/ch05-01-method-syntax.md b/src/ch05-01-method-syntax.md index 0fe0ee7..150b2cb 100644 --- a/src/ch05-01-method-syntax.md +++ b/src/ch05-01-method-syntax.md @@ -2,15 +2,14 @@ > [ch05-01-method-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch05-01-method-syntax.md) >
-> commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c +> commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2 -**方法**与函数类似:他们使用`fn`关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十三章讲解)的上下文中被定义,并且他们第一个参数总是`self`,它代表方法被调用的结构体的实例。 +**方法**与函数类似:他们使用`fn`关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是`self`,它代表方法被调用的结构体的实例。 ### 定义方法 让我们将获取一个`Rectangle`实例作为参数的`area`函数改写成一个定义于`Rectangle`结构体上的`area`方法,如列表 5-7 所示: -
Filename: src/main.rs ```rust @@ -36,12 +35,8 @@ fn main() { } ``` -
- -Listing 5-7: Defining an `area` method on the `Rectangle` struct - -
-
+Listing 5-7: Defining an `area` method on the `Rectangle` +struct @@ -51,7 +46,7 @@ Listing 5-7: Defining an `area` method on the `Rectangle` struct 这里选择`&self`跟在函数版本中使用`&Rectangle`出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将抵押给参数改为`&mut self`。通过仅仅使用`self`作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将`self`转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。 -使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复`self`类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入`impl`块中,而不是让将来的用户在我们的代码中到处寻找`Rectangle的功能。 +使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复`self`类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入`impl`块中,而不是让将来的用户在我们的代码中到处寻找`Rectangle`的功能。 @@ -92,7 +87,6 @@ Listing 5-7: Defining an `area` method on the `Rectangle` struct 让我们更多的实践一下方法,通过为`Rectangle`结构体实现第二个方法。这回,我们让一个`Rectangle`的实例获取另一个`Rectangle`实例并返回`self`能否完全包含第二个长方形,如果能返回`true`若不能则返回`false`。当我们定义了`can_hold`方法,就可以运行列表 5-8 中的代码了: -
Filename: src/main.rs ```rust,ignore @@ -106,12 +100,8 @@ fn main() { } ``` -
- -Listing 5-8: Demonstration of using the as-yet-unwritten `can_hold` method - -
-
+Listing 5-8: Demonstration of using the as-yet-unwritten +`can_hold` method 我们希望看到如下输出,因为`rect2`的长宽都小于`rect1`,而`rect3`比`rect1`要宽: @@ -120,8 +110,7 @@ Can rect1 hold rect2? true Can rect1 hold rect3? false ``` -因为我们想定义一个方法,所以它应该位于`impl Rectangle`块中。方法名是`can_hold`,并且它会获取另一个`Rectangle`的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:`rect1.can_hold(&rect2)`传入了`&rect2`,它是一个`Rectangle`的实例`rect2`的不可变借用。这是可以理解的,因为我们只需要读取`rect2`(而不是写入,这意味着我们需要一个可变借用)而且希望`main`保持`rect2`的所有权这样就可以在调用这个方法后继续使用它。`can_hold`的返回值是一个布尔值,其实现会分别检查`self`的长宽是够都大于另一个`Rectangle`。让我们在列表 5-7 的`impl`块中增加这个新方法: - +因为我们想定义一个方法,所以它应该位于`impl Rectangle`块中。方法名是`can_hold`,并且它会获取另一个`Rectangle`的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:`rect1.can_hold(&rect2)`传入了`&rect2`,它是一个`Rectangle`的实例`rect2`的不可变借用。这是可以理解的,因为我们只需要读取`rect2`(而不是写入,这意味着我们需要一个可变借用)而且希望`main`保持`rect2`的所有权这样就可以在调用这个方法后继续使用它。`can_hold`的返回值是一个布尔值,其实现会分别检查`self`的长宽是够都大于另一个`Rectangle`。让我们在列表 5-7 的`impl`块中增加这个新方法,如列表 5-9 所示: Filename: src/main.rs @@ -143,6 +132,9 @@ impl Rectangle { } ``` +Listing 5-9: Implementing the `can_hold` method on +`Rectangle` that takes another `Rectangle` instance as an argument + 如果结合列表 5-8 的`main`函数来运行,就会看到想要得到的输出!方法可以在`self`后增加多个参数,而且这些参数就像函数中的参数一样工作。 diff --git a/src/ch06-00-enums.md b/src/ch06-00-enums.md index efc6c0e..09a11ca 100644 --- a/src/ch06-00-enums.md +++ b/src/ch06-00-enums.md @@ -2,7 +2,7 @@ > [ch06-00-enums.md](https://github.com/rust-lang/book/blob/master/src/ch06-00-enums.md) >
-> commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d +> commit 4f2dc564851dc04b271a2260c834643dfd86c724 本章介绍**枚举**,也被称作 *enums*。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做`Option`,它代表一个值要么是一些值要么什么都不是。然后会讲到`match`表达式中的模式匹配如何使对枚举不同的值运行不同的代码变得容易。最后会涉及到`if let`,另一个简洁方便处理代码中枚举的结构。 diff --git a/src/ch06-01-defining-an-enum.md b/src/ch06-01-defining-an-enum.md index ca16520..18fd7d0 100644 --- a/src/ch06-01-defining-an-enum.md +++ b/src/ch06-01-defining-an-enum.md @@ -2,7 +2,7 @@ > [ch06-01-defining-an-enum.md](https://github.com/rust-lang/book/blob/master/src/ch06-01-defining-an-enum.md) >
-> commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d +> commit e6d6caab41471f7115a621029bd428a812c5260e 让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以**枚举**出所有可能的值,这也正是它名字的由来。 diff --git a/src/ch16-01-threads.md b/src/ch16-01-threads.md index 1cdd62c..c6699e8 100644 --- a/src/ch16-01-threads.md +++ b/src/ch16-01-threads.md @@ -8,4 +8,267 @@ 我们可以将每个进程运行一个程序的概念再往下抽象一层:程序也可以在其上下文中同时运行独立的部分。这个功能叫做**线程**(*thread*)。 -将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。 \ No newline at end of file +将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程仍然需要以与只期望在单个线程中编程不同的方式思考和组织代码。 + +编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。另外,很多编程语言提供了自己的特殊的线程实现。编程语言提供的线程有时被称作**轻量级**(*lightweight*)或**绿色**(*green*)线程。这些语言将一系列绿色线程放入不同数量的操作系统线程中执行。因为这个原因,语言调用操作系统 API 创建线程的模型有时被称为 *1:1*,一个 OS 线程对应一个语言线程。绿色线程模型被称为 *M:N* 模型,`M`个绿色线程对应`N`个 OS 线程,这里`M`和`N`不必相同。 + +每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。**运行时**是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时不能在为了维持性能而能够在 C 语言中调用方面做出妥协。 + +绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更多的线程控制和更低的上下文切换消耗。 + +现在我们明白了 Rust 中的线程是如何定义的,让我们开始探索如何使用标准库提供的线程相关的 API吧。 + +### 使用`spawn`创建新线程 + +为了创建一个新线程,调用`thread::spawn`函数并传递一个闭包(第十三章学习了闭包),它包含希望在新线程运行的代码。列表 16-1 中的例子在新线程中打印了一些文本而其余的文本在主线程中打印: + +Filename: src/main.rs + +```rust +use std::thread; + +fn main() { + thread::spawn(|| { + for i in 1..10 { + println!("hi number {} from the spawned thread!", i); + } + }); + + for i in 1..5 { + println!("hi number {} from the main thread!", i); + } +} +``` + +Listing 16-1: Creating a new thread to print one thing +while the main thread is printing something else + + +注意这个函数编写的方式,当主线程结束时,它也会停止新线程。这个程序的输出每次可能都略微不同,不过它大体上看起来像这样: + +```text +hi number 1 from the main thread! +hi number 1 from the spawned thread! +hi number 2 from the main thread! +hi number 2 from the spawned thread! +hi number 3 from the main thread! +hi number 3 from the spawned thread! +hi number 4 from the main thread! +hi number 4 from the spawned thread! +hi number 5 from the spawned thread! +``` + +这些线程可能会轮流运行,不过并不保证如此。在这里,主线程先行打印,即便新创建线程的打印语句位于程序的开头。甚至即便我们告诉新建的线程打印直到`i`等于 9 ,它在主线程结束之前也只打印到了 5。如果你只看到了一个线程,或没有出现重叠打印的现象,尝试增加 range 的数值来增加线程暂停并切换到其他线程运行的机会。 + +#### 使用`join`等待所有线程结束 + +由于主线程先于新建线程结束,不仅列表 16-1 中的代码大部分时候不能保证新建线程执行完毕,甚至不能实际保证新建线程会被执行!可以通过保存`thread::spawn`的返回值来解决这个问题,这是一个`JoinHandle`。这看起来如列表 16-2 所示: + +Filename: src/main.rs + +```rust +use std::thread; + +fn main() { + let handle = thread::spawn(|| { + for i in 1..10 { + println!("hi number {} from the spawned thread!", i); + } + }); + + for i in 1..5 { + println!("hi number {} from the main thread!", i); + } + + handle.join(); +} +``` + +Listing 16-2: Saving a `JoinHandle` from `thread::spawn` +to guarantee the thread is run to completion + +`JoinHandle`是一个拥有所有权的值,它可以等待一个线程结束,这也正是`join`方法所做的。通过调用这个句柄的`join`,当前线程会阻塞直到句柄所代表的线程结束。因为我们将`join`调用放在了主线程的`for`循环之后,运行这个例子将产生类似这样的输出: + +``` +hi number 1 from the main thread! +hi number 2 from the main thread! +hi number 1 from the spawned thread! +hi number 3 from the main thread! +hi number 2 from the spawned thread! +hi number 4 from the main thread! +hi number 3 from the spawned thread! +hi number 4 from the spawned thread! +hi number 5 from the spawned thread! +hi number 6 from the spawned thread! +hi number 7 from the spawned thread! +hi number 8 from the spawned thread! +hi number 9 from the spawned thread! +``` + +这两个线程仍然会交替执行,不过主线程会由于`handle.join()`调用会等待直到新建线程执行完毕。 + +如果将`handle.join()`放在主线程的`for`循环之前,像这样: + +Filename: src/main.rs + +```rust +use std::thread; + +fn main() { + let handle = thread::spawn(|| { + for i in 1..10 { + println!("hi number {} from the spawned thread!", i); + } + }); + + handle.join(); + + for i in 1..5 { + println!("hi number {} from the main thread!", i); + } +} +``` + +主线程会等待直到新建线程执行完毕之后才开始执行`for`循环,所以输出将不会交替出现: + +``` +hi number 1 from the spawned thread! +hi number 2 from the spawned thread! +hi number 3 from the spawned thread! +hi number 4 from the spawned thread! +hi number 5 from the spawned thread! +hi number 6 from the spawned thread! +hi number 7 from the spawned thread! +hi number 8 from the spawned thread! +hi number 9 from the spawned thread! +hi number 1 from the main thread! +hi number 2 from the main thread! +hi number 3 from the main thread! +hi number 4 from the main thread! +``` + +稍微考虑一下将`join`放置与何处会影响线程是否同时运行。 + +### 线程和`move`闭包 + +第十三章有一个我们没有讲到的闭包功能,它经常用于`thread::spawn`:`move`闭包。第十三章中讲到: + +> 获取他们环境中值的闭包主要用于开始新线程的场景 + +现在我们正在创建新线程,所以让我们讨论一下获取环境值的闭包吧! + +注意列表 16-1 中传递给`thread::spawn`的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。列表 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作: + +Filename: src/main.rs + +```rust,ignore +use std::thread; + +fn main() { + let v = vec![1, 2, 3]; + + let handle = thread::spawn(|| { + println!("Here's a vector: {:?}", v); + }); + + handle.join(); +} +``` + +Listing 16-3: Attempting to use a vector created by the +main thread from another thread + +闭包使用了`v`,所以闭包会获取`v`并使其成为闭包环境的一部分。因为`thread::spawn`在一个新线程中运行这个闭包,所以可以在新线程中访问`v`。 + +然而当编译这个例子时,会得到如下错误: + +``` +error[E0373]: closure may outlive the current function, but it borrows `v`, +which is owned by the current function + --> + | +6 | let handle = thread::spawn(|| { + | ^^ may outlive borrowed value `v` +7 | println!("Here's a vector: {:?}", v); + | - `v` is borrowed here + | +help: to force the closure to take ownership of `v` (and any other referenced +variables), use the `move` keyword, as shown: + | let handle = thread::spawn(move || { +``` + +当在闭包环境中获取某些值时,Rust 会尝试推断如何获取它。`println!`只需要`v`的一个引用,所以闭包尝试借用`v`。但是这有一个问题:我们并不知道新建线程会运行多久,所以无法知道`v`是否一直时有效的。 + +考虑一下列表 16-4 中的代码,它展示了一个`v`的引用很有可能不再有效的场景: + +Filename: src/main.rs + +```rust,ignore +use std::thread; + +fn main() { + let v = vec![1, 2, 3]; + + let handle = thread::spawn(|| { + println!("Here's a vector: {:?}", v); + }); + + drop(v); // oh no! + + handle.join(); +} +``` + +Listing 16-4: A thread with a closure that attempts to +capture a reference to `v` from a main thread that drops `v` + +这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个`v`的引用,不过主线程仍在执行:它立刻丢弃了`v`,使用了第十五章提到的显式丢弃其参数的`drop`函数。接着,新建线程开始执行,现在`v`是无效的了,所以它的引用也就是无效得的。噢,这太糟了! + +为了修复这个问题,我们可以听取错误信息的建议: + +``` +help: to force the closure to take ownership of `v` (and any other referenced +variables), use the `move` keyword, as shown: + | let handle = thread::spawn(move || { +``` + +通过在闭包之前增加`move`关键字,我们强制闭包获取它使用的值的所有权,而不是引用借用。列表 16-5 中展示的对列表 16-3 代码的修改可以按照我们的预期编译并运行: + +Filename: src/main.rs + +```rust +use std::thread; + +fn main() { + let v = vec![1, 2, 3]; + + let handle = thread::spawn(move || { + println!("Here's a vector: {:?}", v); + }); + + handle.join(); +} +``` + +Listing 16-5: Using the `move` keyword to force a closure +to take ownership of the values it uses + +那么列表 16-4 中那个主线程调用了`drop`的代码该怎么办呢?如果在闭包上增加了`move`,就将`v`移动到了闭包的环境中,我们将不能对其调用`drop`了。相反会出现这个编译时错误: + +``` +error[E0382]: use of moved value: `v` + --> + | +6 | let handle = thread::spawn(move || { + | ------- value moved (into closure) here +... +10 | drop(v); // oh no! + | ^ value used here after move + | + = note: move occurs because `v` has type `std::vec::Vec`, which does + not implement the `Copy` trait +``` + +Rust 的所有权规则又一次帮助了我们! + +现在我们有一个线程和线程 API 的基本了解,让我们讨论一下使用线程实际可以**做**什么吧。 \ No newline at end of file diff --git a/src/ch16-02-message-passing.md b/src/ch16-02-message-passing.md index e69de29..3e8138a 100644 --- a/src/ch16-02-message-passing.md +++ b/src/ch16-02-message-passing.md @@ -0,0 +1,268 @@ +## 使用消息传递在线程间传送数据 + +> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-02-message-passing.md) +>
+> commit da15de39eaabd50100d6fa662c653169254d9175 + +最近人气正在上升的一个并发方式是**消息传递**(*message passing*),这里线程或 actor 通过发送包含数据的消息来沟通。这个思想来源于口号: + +> Do not communicate by sharing memory; instead, share memory by +> communicating. +> +> 不要共享内存来通讯;而是要通讯来共享内存。 +> +> --[Effective Go](http://golang.org/doc/effective_go.html) + +实现这个目标的主要工具是**通道**(*channel*)。通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。代码的一部分可以调用发送者和想要发送的数据,而另一部分代码可以在接收的那一端收取消息。 + +我们将编写一个例子使用一个线程生成值并向通道发送他们。主线程会接收这些值并打印出来。 + +首先,如列表 16-6 所示,先创建一个通道但不做任何事: + +Filename: src/main.rs + +```rust +use std::sync::mpsc; + +fn main() { + let (tx, rx) = mpsc::channel(); +# tx.send(()).unwrap(); +} +``` + +Listing 16-6: Creating a channel and assigning the two +halves to `tx` and `rx` + +`mpsc::channel`函数创建一个新的通道。`mpsc`是**多个生产者,单个消费者**(*multiple producer, single consumer*)的缩写。简而言之,可以有多个产生值的**发送端**,但只能有一个消费这些值的**接收端**。现在我们以一个单独的生产者开始,不过一旦例子可以工作了就会增加多个生产者。 + +`mpsc::channel`返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,很多人使用`tx`和`rx`作为**发送者**和**接收者**的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个`let`语句和模式来解构了元组。第十八章会讨论`let`语句中的模式和解构。 + +让我们将发送端移动到一个新建线程中并发送一个字符串,如列表 16-7 所示: + +Filename: src/main.rs + +```rust +use std::thread; +use std::sync::mpsc; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let val = String::from("hi"); + tx.send(val).unwrap(); + }); +} +``` + +Listing 16-7: Moving `tx` to a spawned thread and sending +"hi" + +正如上一部分那样使用`thread::spawn`来创建一个新线程。并使用一个`move`闭包来将`tx`移动进闭包这样新建线程就是其所有者。 + +通道的发送端有一个`send`方法用来获取需要放入通道的值。`send`方法返回一个`Result`类型,因为如果接收端被丢弃了,将没有发送值的目标,所以发送操作会出错。在这个例子中,我们简单的调用`unwrap`来忽略错误,不过对于一个真实程序,需要合理的处理它。第九章是你复习正确错误处理策略的好地方。 + +在列表 16-8 中,让我们在主线程中从通道的接收端获取值: + +Filename: src/main.rs + +```rust +use std::thread; +use std::sync::mpsc; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let val = String::from("hi"); + tx.send(val).unwrap(); + }); + + let received = rx.recv().unwrap(); + println!("Got: {}", received); +} +``` + +Listing 16-8: Receiving the value "hi" in the main thread +and printing it out + +通道的接收端有两个有用的方法:`recv`和`try_recv`。这里,我们使用了`recv`,它是 *receive* 的缩写。这个方法会阻塞执行直到从通道中接收一个值。一旦发送了一个值,`recv`会在一个`Result`中返回它。当通道发送端关闭,`recv`会返回一个错误。`try_recv`不会阻塞;相反它立刻返回一个`Result`。 + +如果运行列表 16-8 中的代码,我们将会看到主线程打印出这个值: + +``` +Got: hi +``` + +### 通道与所有权如何交互 + +现在让我们做一个试验来看看通道与所有权如何在一起工作:我们将尝试在新建线程中的通道中发送完`val`之后再使用它。尝试编译列表 16-9 中的代码: + +Filename: src/main.rs + +```rust,ignore +use std::thread; +use std::sync::mpsc; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let val = String::from("hi"); + tx.send(val).unwrap(); + println!("val is {}", val); + }); + + let received = rx.recv().unwrap(); + println!("Got: {}", received); +} +``` + +Listing 16-9: Attempting to use `val` after we have sent +it down the channel + +这里尝试在通过`tx.send`发送`val`到通道中之后将其打印出来。这是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们在此使用它之前就修改或者丢弃它。这会由于不一致或不存在的数据而导致错误或意外的结果。 + +尝试编译这些代码,Rust 会报错: + +``` +error[E0382]: use of moved value: `val` + --> src/main.rs:10:31 + | +9 | tx.send(val).unwrap(); + | --- value moved here +10 | println!("val is {}", val); + | ^^^ value used here after move + | + = note: move occurs because `val` has type `std::string::String`, which does + not implement the `Copy` trait +``` + +我们的并发错误会造成一个编译时错误!`send`获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。 + +在这一点上,消息传递非常类似于 Rust 的单所有权系统。消息传递的拥护者出于相似的原因支持消息传递,就像 Rustacean 们欣赏 Rust 的所有权一样:单所有权意味着特定类型问题的消失。如果一次只有一个线程可以使用某些内存,就没有出现数据竞争的机会。 + +### 发送多个值并观察接收者的等待 + +列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂定一段时间。 + +Filename: src/main.rs + +```rust +use std::thread; +use std::sync::mpsc; +use std::time::Duration; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let vals = vec![ + String::from("hi"), + String::from("from"), + String::from("the"), + String::from("thread"), + ]; + + for val in vals { + tx.send(val).unwrap(); + thread::sleep(Duration::new(1, 0)); + } + }); + + for received in rx { + println!("Got: {}", received); + } +} +``` + +Listing 16-10: Sending multiple messages and pausing +between each one + +这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个`Duration`值调用`thread::sleep`函数来暂停一秒。 + +在主线程中,不再显式的调用`recv`函数:而是将`rx`当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。 + +当运行列表 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒: + +``` +Got: hi +Got: from +Got: the +Got: thread +``` + +在主线程中并没有任何暂停或位于`for`循环中用于等待的代码,所以可以说主线程是在等待从新建线程中接收值。 + +### 通过克隆发送者来创建多个生产者 + +差不多在本部分的开头,我们提到了`mpsc`是 *multiple producer, single consumer* 的缩写。可以扩展列表 16-11 中的代码来创建都向同一接收者发送值的多个线程。这可以通过克隆通道的发送端在来做到,如列表 16-11 所示: + +Filename: src/main.rs + +```rust +# use std::thread; +# use std::sync::mpsc; +# use std::time::Duration; +# +# fn main() { +// ...snip... +let (tx, rx) = mpsc::channel(); + +let tx1 = tx.clone(); +thread::spawn(move || { + let vals = vec![ + String::from("hi"), + String::from("from"), + String::from("the"), + String::from("thread"), + ]; + + for val in vals { + tx1.send(val).unwrap(); + thread::sleep(Duration::new(1, 0)); + } +}); + +thread::spawn(move || { + let vals = vec![ + String::from("more"), + String::from("messages"), + String::from("for"), + String::from("you"), + ]; + + for val in vals { + tx.send(val).unwrap(); + thread::sleep(Duration::new(1, 0)); + } +}); +// ...snip... +# +# for received in rx { +# println!("Got: {}", received); +# } +# } +``` + +Listing 16-11: Sending multiple messages and pausing +between each one + +这一次,在创建新线程之前,我们对通道的发送端调用了`clone`方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程,这样每个线程将向通道的接收端发送不同的消息。 + +如果运行这些代码,你**可能**会看到这样的输出: + +``` +Got: hi +Got: more +Got: from +Got: messages +Got: for +Got: the +Got: thread +Got: you +``` + +虽然你可能会看到这些以不同的顺序出现。这依赖于你的系统!这也就是并发既有趣又困难的原因。如果你拿`thread::sleep`做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定并每次都会产生不同的输出。 + +现在我们见识过了通道如何工作,再看看共享内存并发吧。 \ No newline at end of file