diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 75d7dd6..2a41930 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -115,7 +115,6 @@ - [高级特征](ch19-00-advanced-features.md) - [不安全的 Rust](ch19-01-unsafe-rust.md) - - [高级生命周期](ch19-02-advanced-lifetimes.md) - [高级 trait](ch19-03-advanced-traits.md) - [高级类型](ch19-04-advanced-types.md) - [高级函数与闭包](ch19-05-advanced-functions-and-closures.md) diff --git a/src/ch18-03-pattern-syntax.md b/src/ch18-03-pattern-syntax.md index 22b2231..3a045fa 100644 --- a/src/ch18-03-pattern-syntax.md +++ b/src/ch18-03-pattern-syntax.md @@ -2,7 +2,7 @@ > [ch18-03-pattern-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch18-03-pattern-syntax.md) >
-> commit c231bf7e49446e78b147a814323d8f25013a605b +> commit 158c5eb79750d497fe92298a8bee8351c7f3606a 通过本书我们已领略过许多不同类型模式的例子。本节会统一列出所有在模式中有效的语法并且会阐述你为什么可能会希望使用其中的每一个。 @@ -72,20 +72,20 @@ match x { 上面的代码会打印 `one or two`。 -### 通过 `...` 匹配值的范围 +### 通过 `..=` 匹配值的范围 -`...` 语法允许你匹配一个闭区间范围内的值。在如下代码中,当模式匹配任何在此范围内的值时,该分支会执行: +`..=` 语法允许你匹配一个闭区间范围内的值。在如下代码中,当模式匹配任何在此范围内的值时,该分支会执行: ```rust let x = 5; match x { - 1 ... 5 => println!("one through five"), + 1..=5 => println!("one through five"), _ => println!("something else"), } ``` -如果 `x` 是 1、2、3、4 或 5,第一个分支就会匹配。这相比使用 `|` 运算符表达相同的意思更为方便;相比 `1 ... 5`,使用 `|` 则不得不指定 `1 | 2 | 3 | 4 | 5`。相反指定范围就简短的多,特别是在希望匹配比如从 1 到 1000 的数字的时候! +如果 `x` 是 1、2、3、4 或 5,第一个分支就会匹配。这相比使用 `|` 运算符表达相同的意思更为方便;相比 `1..=5`,使用 `|` 则不得不指定 `1 | 2 | 3 | 4 | 5`。相反指定范围就简短的多,特别是在希望匹配比如从 1 到 1000 的数字的时候! 范围只允许用于数字或 `char` 值,因为编译器会在编译时检查范围不为空。`char` 和 数字值是 Rust 仅有的可以判断范围是否为空的类型。 @@ -95,8 +95,8 @@ match x { let x = 'c'; match x { - 'a' ... 'j' => println!("early ASCII letter"), - 'k' ... 'z' => println!("late ASCII letter"), + 'a'..='j' => println!("early ASCII letter"), + 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), } ``` @@ -600,53 +600,6 @@ match msg { 使用 `@` 可以在一个模式中同时测试和保存变量值。 -### 遗留模式: `ref` 和 `ref mut` - -在老版本的 Rust 中,`match` 会假设你希望移动匹配到的值。不过有时并不希望如此。例如: - -```rust -let robot_name = &Some(String::from("Bors")); - -match robot_name { - Some(name) => println!("Found a name: {}", name), - None => (), -} - -println!("robot_name is: {:?}", robot_name); -``` - -这里 `robot_name` 是一个 `&Option`。Rust 会抱怨 `Some(name)` 不匹配 `&Option`,所以不得不这么写: - -```rust,ignore -let robot_name = &Some(String::from("Bors")); - -match robot_name { - &Some(name) => println!("Found a name: {}", name), - None => (), -} - -println!("robot_name is: {:?}", robot_name); -``` - -接着 Rust 会说 `name` 尝试将 `String` 从 option 中移出,不过因为这是一个引用的 option,所以是借用的,因此不能被移动。这就是 `ref` 出场的地方: - -```rust -let robot_name = &Some(String::from("Bors")); - -match robot_name { - &Some(ref name) => println!("Found a name: {}", name), - None => (), -} - -println!("robot_name is: {:?}", robot_name); -``` - -`ref` 关键字就像模式中 `&` 的对立面;它表明 “请将 `ref` 绑定到一个 `&String` 上,不要尝试移动”。换句话说,`&Some` 中的 `&` 匹配的是一个引用,而 `ref` **创建** 了一个引用。`ref mut` 类似 `ref` 不过对应的是可变引用。 - -无论如何,今天的 Rust 不再这样工作。如果尝试 `match` 某些借用的值,那么所有创建的绑定也都会尝试借用。这也意味着之前的代码也能正常工作。 - -因为 Rust 是后向兼容的(backwards compatible),所以不会移除 `ref` 和 `ref mut`,同时它们在一些不明确的场景还有用,比如希望可变地借用结构体的部分值而可变地借用另一部分的情况。你可能会在老的 Rust 代码中看到它们,所以请记住它们仍有价值。 - ## 总结 模式是 Rust 中一个很有用的功能,它帮助我们区分不同类型的数据。当用于 `match` 语句时,Rust 确保模式会包含每一个可能的值,否则程序将不能编译。`let` 语句和函数参数的模式使得这些结构更强大,可以在将值解构为更小部分的同时为变量赋值。可以创建简单或复杂的模式来满足我们的要求。 diff --git a/src/ch19-02-advanced-lifetimes.md b/src/ch19-02-advanced-lifetimes.md deleted file mode 100644 index 24ee170..0000000 --- a/src/ch19-02-advanced-lifetimes.md +++ /dev/null @@ -1,341 +0,0 @@ -## 高级生命周期 - -> [ch19-02-advanced-lifetimes.md](https://github.com/rust-lang/book/blob/master/src/ch19-02-advanced-lifetimes.md) ->
-> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f - -回顾第十章 “生命周期与引用有效性” 部分,我们学习了怎样使用生命周期参数注解引用来帮助 Rust 理解不同引用的生命周期如何相互联系。我们理解了每一个引用都有生命周期,不过大部分情况 Rust 允许我们省略生命周期。这里我们会看到三个还未涉及到的生命周期高级特征: - -* 生命周期子类型(lifetime subtyping),一个确保某个生命周期长于另一个生命周期的方式 -* 生命周期 bound(lifetime bounds),用于指定泛型引用的生命周期 -* trait 对象生命周期(trait object lifetimes),以及他们是如何推断的,以及何时需要指定 -* 匿名生命周期:使(生命周期)省略更为明显 - -### 生命周期子类型确保某个生命周期长于另一个生命周期 - -生命周期子类型是一个指定某个生命周期应该长于另一个生命周期的方式。为了探索生命周期子类型,想象一下我们想要编写一个解析器。为此会有一个储存了需要解析的字符串的引用的结构体 `Context`。解析器将会解析字符串并返回成功或失败。解析器需要借用 `Context` 来进行解析。其实现看起来像示例 19-12 中的代码,除了缺少了必须的生命周期注解,所以这还不能编译: - -文件名: src/lib.rs - -```rust,ignore,does_not_compile -struct Context(&str); - -struct Parser { - context: &Context, -} - -impl Parser { - fn parse(&self) -> Result<(), &str> { - Err(&self.context.0[1..]) - } -} -``` - -示例 19-12: 定义一个不带生命周期注解的解析器 - -编译代码会导致一个表明 Rust 期望 `Context` 中字符串 slice 和 `Parser` 中 `Context` 的引用的生命周期的错误。 - -为了简单起见,`parse` 方法返回 `Result<(), &str>`。也就是说,成功时不做任何操作,失败时则返回字符串 slice 没有正确解析的部分。真实的实现将会包含比这更多的错误信息,并将会在解析成功时返回实际结果,不过我们将去掉这些部分的实现,因为他们与这个例子的生命周期部分并不相关。 - -为了保持代码简单,我们不准备实际编写任何解析逻辑。解析逻辑的某处非常有可能通过返回引用输入中无效部分的错误来处理无效输入,而考虑到生命周期,这个引用是使得这个例子有趣的地方。所以我们将假设解析器的逻辑为输入的第一个字节之后是无效的。注意如果第一个字节并不位于一个有效的字符范围内(比如 Unicode)代码将会 panic;这里又一次简化了例子以专注于涉及到的生命周期。 - -为了使代码能够编译,我们需要放入 `Context` 中字符串 slice 和 `Parser` 中 `Context` 引用的生命周期参数。最直接的方法是在每处都使用相同的生命周期,如示例 19-13 所示。回忆第十章 “结构体定义中的生命周期注解” 部分,`struct Context<'a>`、`struct Parser<'a>` 和 `impl<'a>` 每一个都声明了一个新的生命周期参数。虽然这些名字碰巧都一样,例子中声明的三个生命周期参数并不相关。 - -文件名: src/lib.rs - -```rust -struct Context<'a>(&'a str); - -struct Parser<'a> { - context: &'a Context<'a>, -} - -impl<'a> Parser<'a> { - fn parse(&self) -> Result<(), &str> { - Err(&self.context.0[1..]) - } -} -``` - -示例 19-13: 将所有 `Context` 和 `Parser` 中的引用标注生命周期参数 - -这次可以编译了,并告诉了 Rust `Parser` 存放了一个 `Context` 的引用,拥有生命周期 `'a`,且 `Context` 存放了一个字符串 slice,它也与 `Parser` 中 `Context` 的引用存在的一样久。Rust 编译器的错误信息表明这些引用需要生命周期参数,现在我们增加了这些生命周期参数。 - -接下来,在示例 19-14 中,让我们编写一个获取 `Context` 的实例,使用 `Parser` 来解析其内容,并返回 `parse` 的返回值的函数。这还不能运行: - -文件名: src/lib.rs - -```rust,ignore,does_not_compile -fn parse_context(context: Context) -> Result<(), &str> { - Parser { context: &context }.parse() -} -``` - -示例 19-14: 一个增加获取 `Context` 并使用 `Parser` 的函数 `parse_context` 的尝试 - -当尝试编译这段额外带有 `parse_context` 函数的代码时会得到两个相当冗长的错误: - -```text -error[E0597]: borrowed value does not live long enough - --> src/lib.rs:14:5 - | -14 | Parser { context: &context }.parse() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not live long enough -15 | } - | - temporary value only lives until here - | -note: borrowed value must be valid for the anonymous lifetime #1 defined on the function body at 13:1... - --> src/lib.rs:13:1 - | -13 | / fn parse_context(context: Context) -> Result<(), &str> { -14 | | Parser { context: &context }.parse() -15 | | } - | |_^ - -error[E0597]: `context` does not live long enough - --> src/lib.rs:14:24 - | -14 | Parser { context: &context }.parse() - | ^^^^^^^ does not live long enough -15 | } - | - borrowed value only lives until here - | -note: borrowed value must be valid for the anonymous lifetime #1 defined on the function body at 13:1... - --> src/lib.rs:13:1 - | -13 | / fn parse_context(context: Context) -> Result<(), &str> { -14 | | Parser { context: &context }.parse() -15 | | } - | |_^ -``` - -这些错误表明我们创建的两个 `Parser` 实例和 `context` 参数从 `Parser` 被创建开始一直存活到 `parse_context` 函数结束。不过他们都需要在整个函数的生命周期中都有效。 - -换句话说,`Parser` 和 `context` 需要比整个函数 **长寿**(*outlive*)并在函数开始之前和结束之后都有效以确保代码中的所有引用始终是有效的。虽然我们创建的两个 `Parser` 和 `context` 参数在函数的结尾就离开了作用域,因为 `parse_context` 获取了 `context` 的所有权。 - -为了理解为什么会得到这些错误,让我们再次看看示例 19-13 中的定义,特别是 `parse` 方法的签名中的引用: - -```rust,ignore - fn parse(&self) -> Result<(), &str> { -``` - -还记得(生命周期)省略规则吗?如果标注了引用生命周期而不加以省略,签名看起来应该是这样: - -```rust,ignore - fn parse<'a>(&'a self) -> Result<(), &'a str> { -``` - -正是如此,`parse` 返回值的错误部分的生命周期与 `Parser` 实例的生命周期(`parse` 方法签名中的 `&self`)相绑定。这就可以理解了:因为返回的字符串 slice 引用了 `Parser` 存放的 `Context` 实例中的字符串 slice,同时在 `Parser` 结构体的定义中指定了 `Parser` 中存放的 `Context` 引用的生命周期和 `Context` 中存放的字符串 slice 的生命周期应该一致。 - -问题是 `parse_context` 函数返回 `parse` 的返回值,所以 `parse_context` 返回值的生命周期也与 `Parser` 的生命周期相联系。不过 `parse_context` 函数中创建的 `Parser` 实例并不能存活到函数结束之后(它是临时的),同时 `context` 将会在函数的结尾离开作用域(`parse_context` 获取了它的所有权)。 - -Rust 认为我们尝试返回一个在函数结尾离开作用域的值,因为我们将所有的生命周期都标注为相同的生命周期参数。这告诉了 Rust `Context` 中存放的字符串 slice 的生命周期与 `Parser` 中存放的 `Context` 引用的生命周期一致。 - -`parse_context` 函数并不知道 `parse` 函数里面是什么,返回的字符串 slice 将比 `Context` 和 `Parser` 都存活的更久,同时 `parse_context` 返回的引用指向字符串 slice,而不是 `Context` 或 `Parser`。 - -通过了解 `parse` 实现所做的工作,可以知道 `parse` 的返回值(的生命周期)与 `Parser` 相联系的唯一理由是它引用了 `Parser` 的 `Context`,也就是引用了这个字符串 slice,这正是 `parse_context` 所需要关心的生命周期。需要一个方法来告诉 Rust `Context` 中的字符串 slice 与 `Parser` 中 `Context` 的引用有着不同的生命周期,而且 `parse_context` 返回值与 `Context` 中字符串 slice 的生命周期相联系。 - -首先尝试像示例 19-15 那样给予 `Parser` 和 `Context` 不同的生命周期参数。这里选择了生命周期参数名 `'s` 和 `'c` 是为了使得 `Context` 中字符串 slice 与 `Parser` 中 `Context` 引用的生命周期显得更明了(英文首字母)。注意这并不能完全解决问题,不过这是一个开始,我们将看看为什么这还不足以能够编译代码。 - -文件名: src/lib.rs - -```rust,ignore,does_not_compile -struct Context<'s>(&'s str); - -struct Parser<'c, 's> { - context: &'c Context<'s>, -} - -impl<'c, 's> Parser<'c, 's> { - fn parse(&self) -> Result<(), &'s str> { - Err(&self.context.0[1..]) - } -} - -fn parse_context(context: Context) -> Result<(), &str> { - Parser { context: &context }.parse() -} -``` - -示例 19-15: 为字符串 slice 和 `Context` 的引用指定不同的生命周期参数 - -这里在与示例 19-13 完全相同的地方标注了引用的生命周期,不过根据引用是字符串 slice 或 `Context` 与否使用了不同的参数。另外还在 `parse` 返回值的字符串 slice 部分增加了注解来表明它与 `Context` 中字符串 slice 的生命周期相关联。 - -这里是现在尝试编译时得到的错误: - -```text -error[E0491]: in type `&'c Context<'s>`, reference has a longer lifetime than the data it references - --> src/lib.rs:4:5 - | -4 | context: &'c Context<'s>, - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | -note: the pointer is valid for the lifetime 'c as defined on the struct at 3:1 - --> src/lib.rs:3:1 - | -3 | / struct Parser<'c, 's> { -4 | | context: &'c Context<'s>, -5 | | } - | |_^ -note: but the referenced data is only valid for the lifetime 's as defined on the struct at 3:1 - --> src/lib.rs:3:1 - | -3 | / struct Parser<'c, 's> { -4 | | context: &'c Context<'s>, -5 | | } - | |_^ -``` - -Rust 并不知道 `'c` 与 `'s` 之间的任何联系。为了保证有效性,`Context` 中引用的带有生命周期 `'s` 的数据需要遵守它比带有生命周期 `'c` 的 `Context` 的引用存活得更久的保证。如果 `'s` 不比 `'c` 更长久,那么 `Context` 的引用可能不再有效。 - -这就引出了本部分的要点:Rust 的 **生命周期子类型**(*lifetime subtyping*)功能,这是一个指定一个生命周期不会短于另一个的方法。在声明生命周期参数的尖括号中,可以照常声明一个生命周期 `'a`,并通过语法 `'b: 'a` 声明一个不短于 `'a` 的生命周期 `'b`。 - -在 `Parser` 的定义中,为了表明 `'s`(字符串 slice 的生命周期)保证至少与 `'c`(`Context` 引用的生命周期)一样长,需将生命周期声明改为如此: - -文件名: src/lib.rs - -```rust -# struct Context<'a>(&'a str); -# -struct Parser<'c, 's: 'c> { - context: &'c Context<'s>, -} -``` - -现在 `Parser` 中 `Context` 的引用与 `Context` 中字符串 slice 就有了不同的生命周期,并且保证了字符串 slice 的生命周期比 `Context` 引用的要长。 - -这是一个非常冗长的例子,不过正如本章的开头所提到的,这类功能是很小众的。你并不会经常需要这个语法,不过当出现类似这样的情形时,却还是有地方可以参考的。 - -### 生命周期 bound 用于泛型的引用 - -在第十章 “trait bound” 部分,我们讨论了如何在泛型类型上使用 trait bound。也可以像泛型那样为生命周期参数增加限制,这被称为 **生命周期 bound**(*lifetime bounds*)。生命周期 bound 帮助 Rust 验证泛型的引用不会存在的比其引用的数据更久。 - -例如,考虑一下一个封装了引用的类型。回忆一下第十五章 “`RefCell` 和内部可变性模式” 部分的 `RefCell` 类型:其 `borrow` 和 `borrow_mut` 方法分别返回 `Ref` 和 `RefMut` 类型。这些类型是引用的封装,他们在运行时记录检查借用规则。`Ref` 结构体的定义如示例 19-16 所示,目前还不带有生命周期 bound: - -文件名: src/lib.rs - -```rust,ignore,does_not_compile -struct Ref<'a, T>(&'a T); -``` - -示例 19-16: 定义结构体来封装泛型的引用,没有生命周期 bound - -若不显式限制生命周期 `'a` 为与泛型参数 `T` 有关,会得到一个错误因为 Rust 不知道泛型 `T` 会存活多久: - -```text -error[E0309]: the parameter type `T` may not live long enough - --> src/lib.rs:1:19 - | -1 | struct Ref<'a, T>(&'a T); - | ^^^^^^ - | - = help: consider adding an explicit lifetime bound `T: 'a`... -note: ...so that the reference type `&'a T` does not outlive the data it points at - --> src/lib.rs:1:19 - | -1 | struct Ref<'a, T>(&'a T); - | ^^^^^^ -``` - -因为 `T` 可以是任意类型,`T` 自身也可能是一个引用,或者是一个存放了一个或多个引用的类型,而他们各自可能有着不同的生命周期。Rust 不能确认 `T` 会与 `'a` 存活的一样久。 - -幸运的是,Rust 提供了这个情况下如何指定生命周期 bound 的有用建议: - -```text -consider adding an explicit lifetime bound `T: 'a` so that the reference type -`&'a T` does not outlive the data it points at -``` - -示例 19-17 展示了如何按照这个建议,在声明泛型 `T` 时指定生命周期 bound。。 - -```rust -struct Ref<'a, T: 'a>(&'a T); -``` - -示例 19-17: 为 `T` 增加生命周期 bound 来指定 `T` 中的任何引用需至少与 `'a` 存活的一样久 - -现在代码可以编译了,因为 `T: 'a` 语法指定了 `T` 可以为任意类型,不过如果它包含任何引用的话,其生命周期必须至少与 `'a` 一样长。 - -我们可以选择不同的方法来解决这个问题,如示例 19-18 中 `StaticRef` 的结构体定义所示,通过在 `T` 上增加 `'static` 生命周期约束。这意味着如果 `T` 包含任何引用,他们必须有 `'static` 生命周期: - -```rust -struct StaticRef(&'static T); -``` - -示例 19-18: 在 `T` 上增加 `'static` 生命周期 bound,来限制 `T` 为只拥有 `'static` 生命周期的引用或没有引用的类型 - -因为 `'static` 意味着引用必须同整个程序存活的一样长,一个不包含引用的类型满足所有引用都与整个程序存活的一样长的标准(因为他们没有引用)。对于借用检查器来说它关心的是引用是否存活的足够久,没有引用的类型与有永远存在的引用的类型并没有真正的区别;对于确定引用是否比其所引用的值存活得较短的目的来说两者是一样的。 - -### trait 对象生命周期的推断 - -在第十七章的 “为使用不同类型的值而设计的 trait 对象” 部分,我们讨论了 trait 对象,它包含一个位于引用之后的 trait,这允许我们进行动态分发。我们所没有讨论的是如果 trait 对象中实现 trait 的类型带有生命周期时会发生什么。考虑一下示例 19-19,其中有 trait `Red` 和结构体 `Ball`。`Ball` 存放了一个引用(因此有一个生命周期参数)并实现了 trait `Red`。我们希望使用一个作为 trait 对象 `Box` 的 `Ball` 实例: - -文件名: src/main.rs - -```rust -trait Red { } - -struct Ball<'a> { - diameter: &'a i32, -} - -impl<'a> Red for Ball<'a> { } - -fn main() { - let num = 5; - - let obj = Box::new(Ball { diameter: &num }) as Box; -} -``` - -示例 19-19: 使用一个带有生命周期的类型用于 trait 对象 - -这段代码能没有任何错误的编译,即便并没有明确指出 `obj` 中涉及的任何生命周期。这是因为有如下生命周期与 trait 对象必须遵守的规则: - -* trait 对象的默认生命周期是 `'static`。 -* 如果有 `&'a X` 或 `&'a mut X`,则默认生命周期是 `'a`。 -* 如果只有 `T: 'a` 从句, 则默认生命周期是 `'a`。 -* 如果有多个类似 `T: 'a` 的从句,则没有默认生命周期;必须明确指定。 - -当必须明确指定时,可以为像 `Box` 这样的 trait 对象增加生命周期 bound,根据需要使用语法 `Box` 或 `Box`。正如其他的 bound,这意味着任何 `Red` trait 的实现如果在内部包含有引用, 这些引用就必须拥有与 trait 对象 bound 中所指定的相同的生命周期。 - -### 匿名生命周期 - -比方说有一个封装了一个字符串 slice 的结构体,如下: - -```rust -struct StrWrap<'a>(&'a str); -``` - -可以像这样编写一个返回它们的函数: - -```rust -# struct StrWrap<'a>(&'a str); -fn foo<'a>(string: &'a str) -> StrWrap<'a> { - StrWrap(string) -} -``` - -不过这里有很多的 `'a`!为了消除这些噪音,可以使用匿名生命周期,`'_`,如下: - -```rust -# struct StrWrap<'a>(&'a str); -fn foo(string: &str) -> StrWrap<'_> { - StrWrap(string) -} -``` - -`'_` 表明 “在此处使用省略的生命周期。” 这意味着我们仍然知道 `StrWrap` 包含一个引用,不过无需所有的生命周期注解来知道。 - -其也能用于 `impl`;例如: - -```rust,ignore -// 冗余 -impl<'a> fmt::Debug for StrWrap<'a> { - -// 省略 -impl fmt::Debug for StrWrap<'_> { - -``` - -接下来,让我们看看一些其他处理 trait 的高级功能吧! diff --git a/src/ch20-04-storing-threads.md b/src/ch20-04-storing-threads.md deleted file mode 100644 index e9a7e0f..0000000 --- a/src/ch20-04-storing-threads.md +++ /dev/null @@ -1,185 +0,0 @@ -## 创建线程池并储存线程 - -> [ch20-04-storing-threads.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-04-storing-threads.md) ->
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 - -之前的警告是因为在 `new` 和 `execute` 中没有对参数做任何操作。让我们用期望的实际行为实现他们。 - -### 验证池中的线程数 - -以考虑 `new` 作为开始。之前提到使用无符号类型作为 `size` 参数的类型,因为为负的线程数没有意义。然而,零个线程同样没有意义,不过零是一个完全有效的 `u32` 值。让我们在返回 `ThreadPool` 之前检查 `size` 是否大于零,并使用 `assert!` 宏在得到零时 panic,如列表 20-13 所示: - -文件名: src/lib.rs - -```rust -# pub struct ThreadPool; -impl ThreadPool { - /// Create a new ThreadPool. - /// - /// The size is the number of threads in the pool. - /// - /// # Panics - /// - /// The `new` function will panic if the size is zero. - pub fn new(size: u32) -> ThreadPool { - assert!(size > 0); - - ThreadPool - } - - // ...snip... -} -``` - -列表 20-13:实现 `ThreadPool::new` 在 `size` 为零时 panic - -趁着这个机会我们用文档注释为 `ThreadPool` 增加了一些文档。注意这里遵循了良好的文档实践并增加了一个部分提示函数会 panic 的情况,正如第十四章所讨论的。尝试运行 `cargo doc --open` 并点击 `ThreadPool` 结构体来查看生成的 `new` 的文档看起来如何! - -相比像这里使用 `assert!` 宏,也可以让 `new` 像之前 I/O 项目中列表 12-9 中 `Config::new` 那样返回一个 `Result`,不过在这里我们选择创建一个没有任何线程的线程池应该是要给不可恢复的错误。如果你想做的更好,尝试编写一个采用如下签名的 `new` 版本来感受一下两者的区别: - -```rust -fn new(size: u32) -> Result { -``` - -### 在线程池中储存线程 - -现在有了一个有效的线程池线程数,就可以实际创建这些线程并在返回之前将他们储存在 `ThreadPool` 结构体中。 - -这引出了另一个问题:如何“储存”一个线程?让我们再看看 `thread::spawn` 的签名: - -```rust -pub fn spawn(f: F) -> JoinHandle - where - F: FnOnce() -> T + Send + 'static, - T: Send + 'static -``` - -`spawn` 返回 `JoinHandle`,其中 `T` 是闭包返回的类型。尝试使用 `JoinHandle` 来看看会发生什么。在我们的情况中,传递给线程池的闭包会处理连接并不返回任何值,所以 `T` 将会是单元类型 `()`。 - -这还不能编译,不过考虑一下列表 20-14 所示的代码。我们改变了 `ThreadPool` 的定义来存放一个 `thread::JoinHandle<()>` 的 vector 实例,使用 `size` 容量来初始化,并设置一个 `for` 循环了来运行创建线程的代码,并返回包含这些线程的 `ThreadPool` 实例: - - -文件名: src/lib.rs - -```rust,ignore -use std::thread; - -pub struct ThreadPool { - threads: Vec>, -} - -impl ThreadPool { - // ...snip... - pub fn new(size: u32) -> ThreadPool { - assert!(size > 0); - - let mut threads = Vec::with_capacity(size); - - for _ in 0..size { - // create some threads and store them in the vector - } - - ThreadPool { - threads - } - } - - // ...snip... -} -``` - -列表 20-14:为 `ThreadPool` 创建一个 vector 来存放线程 - -这里将 `std::thread` 引入库 crate 的作用域,因为使用了 `thread::JoinHandle` 作为 `ThreadPool` 中 vector 元素的类型。 - -在得到了有效的数量之后,就可以新建一个存放 `size` 个元素的 vector。本书还未使用过 `with_capacity`;它与 `Vec::new` 做了同样的工作,不过有一个重要的区别:它为 vector 预先分配空间。因为已经知道了 vector 中需要 `size` 个元素,预先进行分配比仅仅 `Vec::new` 要稍微有效率一些,因为 `Vec::new` 随着插入元素而重新改变大小。因为一开始就用所需的确定大小来创建 vector,为其增减元素时不会改变底层 vector 的大小。 - -如果代码能够工作就应是如此效果,不过他们还不能工作!如果检查他们,会得到一个错误: - -``` -$ cargo check - Compiling hello v0.1.0 (file:///projects/hello) -error[E0308]: mismatched types - --> src\main.rs:70:46 - | -70 | let mut threads = Vec::with_capacity(size); - | ^^^^ expected usize, found u32 - -error: aborting due to previous error -``` - -`size` 是 `u32`,不过 `Vec::with_capacity` 需要一个 `usize`。这里有两个选择:可以改变函数签名,或者可以将 `u32` 转换为 `usize`。如果你还记得定义 `new` 时,并没有仔细考虑有意义的数值类型,只是随便选了一个。现在来进行一些思考吧。考虑到 `size` 是 vector 的长度,`usize` 就很有道理了。甚至他们的名字都很类似!改变 `new` 的签名,这会使列表 20-14 的代码能够编译: - -```rust -fn new(size: usize) -> ThreadPool { -``` - -如果再次运行 `cargo check`,会得到一些警告,不过应该能成功编译。 - -列表 20-14 的 `for` 循环中留下了一个关于创建线程的注释。如何实际创建线程呢?这是一个难题。这些线程应该做什么呢?这里并不知道他们需要做什么,因为 `execute` 方法获取闭包并传递给了线程池。 - -让我们稍微重构一下:不再储存一个 `JoinHandle<()>` 实例的 vector,将创建一下新的结构体来代表 *worker* 的概念。worker 会接收 `execute` 方法,并会处理实际的闭包调用。另外储存固定 `size` 数量的还不知道将要执行什么闭包的 `Worker` 实例,也可以为每一个 worker 设置一个 `id`,这样就可以在日志和调试中区别线程池中的不同 worker。 - -让我们做出如下修改: - -1. 定义 `Worker` 结构体存放 `id` 和 `JoinHandle<()>` -2. 修改 `ThreadPool` 存放一个 `Worker` 实例的 vector -3. 定义 `Worker::new` 函数,它获取一个 `id` 数字并返回一个带有 `id` 和用空闭包分配的线程的 `Worker` 实例,之后会修复这些 -4. 在 `ThreadPool::new` 中,使用 `for` 循环来计数生成 `id`,使用这个 `id` 新建 `Worker`,并储存进 vector 中 - -如果你渴望挑战,在查看列表 20-15 中的代码之前尝试自己实现这些修改。 - -准备好了吗?列表 20-15 就是一个做出了这些修改的例子: - -文件名: src/lib.rs - -```rust -use std::thread; - -pub struct ThreadPool { - workers: Vec, -} - -impl ThreadPool { - // ...snip... - pub fn new(size: usize) -> ThreadPool { - assert!(size > 0); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id)); - } - - ThreadPool { - workers - } - } - // ...snip... -} - -struct Worker { - id: usize, - thread: thread::JoinHandle<()>, -} - -impl Worker { - fn new(id: usize) -> Worker { - let thread = thread::spawn(|| {}); - - Worker { - id, - thread, - } - } -} -``` - -列表 20-15:修改 `ThreadPool` 存放 `Worker` 实例而不是直接存放线程 - -这里选择将 `ThreadPool` 中字段名从 `threads` 改为 `workers`,因为我们改变了存放内容为 `Worker` 而不是 `JoinHandle<()>`。使用 `for` 循环中的计数作为 `Worker::new` 的参数,并将每一个新建的 `Worker` 储存在叫做 `workers` 的 vector 中。 - -`Worker` 结构体和其 `new` 函数是私有的,因为外部代码(比如 *src/bin/main.rs* 中的 server)并不需要 `ThreadPool` 中使用 `Worker` 结构体的实现细节。`Worker::new` 函数使用 `id` 参数并储存了使用一个空闭包创建的 `JoinHandle<()>`。 - -这段代码能够编译并用指定给 `ThreadPool::new` 的参数创建储存了一系列的 `Worker` 实例,不过**仍然**没有处理 `execute` 中得到的闭包。让我们聊聊接下来怎么做。 diff --git a/src/ch20-05-sending-requests-via-channels.md b/src/ch20-05-sending-requests-via-channels.md deleted file mode 100644 index 9c33271..0000000 --- a/src/ch20-05-sending-requests-via-channels.md +++ /dev/null @@ -1,394 +0,0 @@ -## 使用通道向线程发送请求 - -> [ch20-05-sending-requests-via-channels.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-05-sending-requests-via-channels.md) ->
-> commit 2e269ff82193fd65df8a87c06561d74b51ac02f7 - -下一个需要解决的问题是(线程中的)闭包完全没有做任何工作。我们一直在绕过获取 `execute` 方法中实际期望执行的闭包的问题,不过看起来在创建 `ThreadPool` 时就需要知道实际的闭包。 - -不过考虑一下真正需要做的:我们希望刚创建的 `Worker` 结构体能够从 `ThreadPool` 的队列中获取任务,并在线程中执行他们。 - -在第十六章中,我们学习了通道。通道是一个沟通两个线程的良好手段,对于这个例子来说则是绝佳的。通道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划: - -1. `ThreadPool` 会创建一个通道并充当发送端。 -2. 每个 `Worker` 将会充当通道的接收端。 -3. 新建一个 `Job` 结构体来存放用于向通道中发送的闭包。 -4. `ThreadPool` 的 `execute` 方法会在发送端发出期望执行的任务。 -5. 在线程中,`Worker` 会遍历通道的接收端并执行任何接收到的任务。 - -让我们以在 `ThreadPool::new` 中创建通道并让 `ThreadPool` 实例充当发送端开始,如列表 20-16 所示。`Job` 是将在通道中发出的类型;目前它是一个没有任何内容的结构体: - -文件名: src/lib.rs - -```rust -# use std::thread; -// ...snip... -use std::sync::mpsc; - -pub struct ThreadPool { - workers: Vec, - sender: mpsc::Sender, -} - -struct Job; - -impl ThreadPool { - // ...snip... - pub fn new(size: usize) -> ThreadPool { - assert!(size > 0); - - let (sender, receiver) = mpsc::channel(); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id)); - } - - ThreadPool { - workers, - sender, - } - } - // ...snip... -} -# -# struct Worker { -# id: usize, -# thread: thread::JoinHandle<()>, -# } -# -# impl Worker { -# fn new(id: usize) -> Worker { -# let thread = thread::spawn(|| {}); -# -# Worker { -# id, -# thread, -# } -# } -# } -``` - -列表 20-16:修改 `ThreadPool` 来储存一个发送 `Job` 实例的通道发送端 - -在 `ThreadPool::new` 中,新建了一个通道,并接着让线程池在接收端等待。这段代码能够编译,不过仍有警告。 - -在线程池创建每个 worker 时将通道的接收端传递给他们。须知我们希望在 worker 所分配的线程中使用通道的接收端,所以将在闭包中引用 `receiver` 参数。列表 20-17 中展示的代码还不能编译: - -文件名: src/lib.rs - -```rust,ignore -impl ThreadPool { - // ...snip... - pub fn new(size: usize) -> ThreadPool { - assert!(size > 0); - - let (sender, receiver) = mpsc::channel(); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id, receiver)); - } - - ThreadPool { - workers, - sender, - } - } - // ...snip... -} - -// ...snip... - -impl Worker { - fn new(id: usize, receiver: mpsc::Receiver) -> Worker { - let thread = thread::spawn(|| { - receiver; - }); - - Worker { - id, - thread, - } - } -} -``` - -列表 20-17:将通道的接收端传递给 worker - -这是一些小而直观的修改:将通道的接收端传递进了 `Worker::new`,并接着在闭包中使用他们。 - -如果尝试检查代码,会得到这个错误: - -``` -$ cargo check - Compiling hello v0.1.0 (file:///projects/hello) -error[E0382]: use of moved value: `receiver` - --> src/lib.rs:27:42 - | -27 | workers.push(Worker::new(id, receiver)); - | ^^^^^^^^ value moved here in - previous iteration of loop - | - = note: move occurs because `receiver` has type - `std::sync::mpsc::Receiver`, which does not implement the `Copy` trait -``` - -这些代码还不能编译的原因如上因为它尝试将 `receiver` 传递给多个 `Worker` 实例。回忆第十六章,Rust 所提供的通道实现是多**生产者**,单**消费者**的,所以不能简单的克隆通道的消费端来解决问题。即便可以我们也不希望克隆消费端;在所有的 worker 中共享单一 `receiver` 才是我们希望的在线程间分发任务的机制。 - -另外,从通道队列中取出任务涉及到修改 `receiver`,所以这些线程需要一个能安全的共享和修改 `receiver` 的方式。如果修改不是线程安全的,则可能遇到竞争状态,例如两个线程因同时在队列中取出相同的任务并执行了相同的工作。 - -所以回忆一下第十六章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。列表 20-18 展示了所做的修改: - -文件名: src/lib.rs - -```rust -# use std::thread; -# use std::sync::mpsc; -use std::sync::Arc; -use std::sync::Mutex; - -// ...snip... - -# pub struct ThreadPool { -# workers: Vec, -# sender: mpsc::Sender, -# } -# struct Job; -# -impl ThreadPool { - // ...snip... - pub fn new(size: usize) -> ThreadPool { - assert!(size > 0); - - let (sender, receiver) = mpsc::channel(); - - let receiver = Arc::new(Mutex::new(receiver)); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id, receiver.clone())); - } - - ThreadPool { - workers, - sender, - } - } - - // ...snip... -} -# struct Worker { -# id: usize, -# thread: thread::JoinHandle<()>, -# } -# -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - // ...snip... -# let thread = thread::spawn(|| { -# receiver; -# }); -# -# Worker { -# id, -# thread, -# } - } -} -``` - -列表 20-18:使用 `Arc` 和 `Mutex` 在 worker 间共享通道的接收端 - -在 `ThreadPool::new` 中,将通道的接收端放入一个 `Arc` 和一个 `Mutex` 中。对于每一个新 worker,则克隆 `Arc` 来增加引用计数,如此这些 worker 就可以共享接收端的所有权了。 - -通过这些修改,代码可以编译了!我们做到了! - -最好让我们实现 `ThreadPool` 上的 `execute` 方法。同时也要修改 `Job` 结构体:它将不再是结构体,`Job` 将是一个有着 `execute` 接收到的闭包类型的 trait 对象的类型别名。我们讨论过类型别名如何将长的类型变短,现在就这种情况!看一看列表 20-19: - -文件名: src/lib.rs - -```rust -// ...snip... -# pub struct ThreadPool { -# workers: Vec, -# sender: mpsc::Sender, -# } -# use std::sync::mpsc; -# struct Worker {} - -type Job = Box; - -impl ThreadPool { - // ...snip... - - pub fn execute(&self, f: F) - where - F: FnOnce() + Send + 'static - { - let job = Box::new(f); - - self.sender.send(job).unwrap(); - } -} - -// ...snip... -``` - -列表 20-19:为存放每一个闭包的 `Box` 创建一个 `Job` 类型别名,接着在通道中发出 - -在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从通道的发送端发出。这里调用 `send` 上的 `unwrap`,因为如果接收端停止接收新消息则发送可能会失败,这可能发生于我们停止了所有的执行线程。不过目前这是不可能的,因为只要线程池存在他们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,即便编译器不这么认为,正如第九章讨论的这是 `unwrap` 的一个恰当用法。 - -那我们结束了吗?不完全是!在 worker 中,传递给 `thread::spawn` 的闭包仍然还只是**引用**了通道的接收端。但是我们需要闭包一直循环,向通道的接收端请求任务,并在得到任务时执行他们。如列表 20-20 对 `Worker::new` 做出修改: - -文件名: src/lib.rs - -```rust -// ...snip... - -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || { - loop { - let job = receiver.lock().unwrap().recv().unwrap(); - - println!("Worker {} got a job; executing.", id); - - (*job)(); - } - }); - - Worker { - id, - thread, - } - } -} -``` - -列表 20-20: 在 worker 线程中接收并执行任务 - -这里,首先在 `receiver` 上调用了 `lock` 来获取互斥器,接着 `unwrap` 在出现任何错误时 panic。如果互斥器处于一种叫做**被污染**(*poisoned*)的状态时获取锁肯能会失败,这可能发生于其他线程在持有锁时 panic 了并没有释放锁。如果当前线程因为这个原因不能得到所,调用 `unwrap` 使其 panic 也是正确的行为。如果你觉得有意义的话请随意将 `unwrap` 改为带有错误信息的 `expect`。 - -如果锁定了互斥器,接着调用 `recv` 从通道中接收 `Job`。最后的 `unwrap` 也绕过了一些错误,`recv` 在通道的发送端关闭时会返回 `Err`,类似于 `send` 在接收端关闭时返回 `Err` 一样。 - -调用 `recv` 的代码块;也就是说,它还没有任务,这个线程会等待直到有可用的任务。`Mutex` 确保一次只有一个 `Worker` 线程尝试请求任务。 - -理论上这段代码应该能够编译。不幸的是,Rust 编译器仍不够完美,会给出如下错误: - -``` -error[E0161]: cannot move a value of type std::ops::FnOnce() + -std::marker::Send: the size of std::ops::FnOnce() + std::marker::Send cannot be -statically determined - --> src/lib.rs:63:17 - | -63 | (*job)(); - | ^^^^^^ -``` - -这个错误非常的神秘,因为这个问题本身就很神秘。为了调用储存在 `Box` (这正是 `Job` 别名的类型)中的 `FnOnce` 闭包,该闭包需要能将自己移动出 `Box`,因为当调用这个闭包时,它获取 `self` 的所有权。通常来说,将值移动出 `Box` 是不被允许的,因为 Rust 不知道 `Box` 中的值将会有多大;回忆第十五章能够正常使用 `Box` 是因为我们将未知大小的值储存进 `Box` 从而得到已知大小的值。 - -第十七章曾见过,列表 17-15 中有使用了 `self: Box` 语法的方法,它获取了储存在 `Box` 中的 `Self` 值的所有权。这正是我们希望做的,然而不幸的是 Rust 调用闭包的那部分实现并没有使用 `self: Box`。所以这里 Rust 也不知道它可以使用 `self: Box` 来获取闭包的所有权并将闭包移动出 `Box`。 - -将来列表 20-20 中的代码应该能够正常工作。Rust 仍在努力改进提升编译器。有很多像你一样的人正在修复这个以及其他问题!当你结束了本书的阅读,我们希望看到你也成为他们中的一员。 - -不过目前让我们绕过这个问题。所幸有一个技巧可以显式的告诉 Rust 我们处于可以获取使用 `self: Box` 的 `Box` 中值的所有权的状态,而一旦获取了闭包的所有权就可以调用它了。这涉及到定义一个新 trait,它带有一个在签名中使用 `self: Box` 的方法 `call_box`,为任何实现了 `FnOnce()` 的类型定义这个 trait,修改类型别名来使用这个新 trait,并修改 `Worker` 使用 `call_box` 方法。这些修改如列表 20-21 所示: - -文件名: src/lib.rs - -```rust -trait FnBox { - fn call_box(self: Box); -} - -impl FnBox for F { - fn call_box(self: Box) { - (*self)() - } -} - -type Job = Box; - -// ...snip... - -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || { - loop { - let job = receiver.lock().unwrap().recv().unwrap(); - - println!("Worker {} got a job; executing.", id); - - job.call_box(); - } - }); - - Worker { - id, - thread, - } - } -} -``` - -列表 20-21:新增一个 trait `FnBox` 来绕过当前 `Box` 的限制 - -首先,新建了一个叫做 `FnBox` 的 trait。这个 trait 有一个方法 `call_box`,它类似于其他 `Fn*` trait 中的 `call` 方法,除了它获取 `self: Box` 以便获取 `self` 的所有权并将值从 `Box` 中移动出来。 - -现在我们希望 `Job` 类型别名是任何实现了新 trait `FnBox` 的 `Box`,而不是 `FnOnce()`。这允许我们在得到 `Job` 值时使用 `Worker` 中的 `call_box`。因为我们为任何 `FnOnce()` 闭包都实现了 `FnBox` trait,无需对实际在通道中发出的值做任何修改。 - -最后,对于 `Worker::new` 的线程中所运行的闭包,调用 `call_box` 而不是直接执行闭包。现在 Rust 就能够理解我们的行为是正确的了。 - -这是非常狡猾且复杂的手段。无需过分担心他们并不是非常有道理;总有一天,这一切将是毫无必要的。 - -通过这些技巧,线程池处于可以运行的状态了!执行 `cargo run` 并发起一些请求: - -``` -$ cargo run - Compiling hello v0.1.0 (file:///projects/hello) -warning: field is never used: `workers` - --> src/lib.rs:7:5 - | -7 | workers: Vec, - | ^^^^^^^^^^^^^^^^^^^^ - | - = note: #[warn(dead_code)] on by default - -warning: field is never used: `id` - --> src/lib.rs:61:5 - | -61 | id: usize, - | ^^^^^^^^^ - | - = note: #[warn(dead_code)] on by default - -warning: field is never used: `thread` - --> src/lib.rs:62:5 - | -62 | thread: thread::JoinHandle<()>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: #[warn(dead_code)] on by default - - Finished dev [unoptimized + debuginfo] target(s) in 0.99 secs - Running `target/debug/hello` - Worker 0 got a job; executing. -Worker 2 got a job; executing. -Worker 1 got a job; executing. -Worker 3 got a job; executing. -Worker 0 got a job; executing. -Worker 2 got a job; executing. -Worker 1 got a job; executing. -Worker 3 got a job; executing. -Worker 0 got a job; executing. -Worker 2 got a job; executing. -``` - -成功了!现在我们有了一个可以异步执行连接的线程池!它绝不会创建超过四个线程,所以当 server 收到大量请求时系统也不会负担过重。如果请求 `/sleep`,server 也能够通过另外一个线程处理其他请求。 - -那么这些警告怎么办呢?难道我们没有使用 `workers`、`id` 和 `thread` 字段吗?好吧,目前我们用了所有这些字段存放了一些数据,不过当设置线程池并开始执行代码在通道中向线程发送任务时,我们并没有对数据**进行**任何实际的操作。但是如果不存放这些值,他们将会离开作用域:比如,如果不将 `Vec` 值作为 `ThreadPool` 的一部分返回,这个 vector 在 `ThreadPool::new` 的结尾就会被清理。 - -那么这些警告有错吗?从某种角度上讲是的,这些警告是错误的,因为我们使用这些字段储存一直需要的数据。从另一种角度来说也不对:使用过后我们也没有做任何操作清理线程池,仅仅通过 ctrl-C 来停止程序并让操作系统为我们清理。下面让我们实现 graceful shutdown 来清理所创建的一切。