Rust网页版ide
https://play.rust-lang.org/
安装配置
RustUp相关
rustup show
rustup update
rustup self uninstall
rustup default stable-x86_64-pc-windows-gnu
rustup -h
rustup toolchain list
rustup toolchain install stable-x86_64-pc-windows-gnu
rustup toolchain uninstall stable-x86_64-pc-windows-gnu
rustup toolchain link <toolchain-name> "<toolchain-path>"
rustup override list
rustup override set <toolchain> --path <path>
rustup override unset --path <path>
rustup target list
rustup target add <target>
rustup target remove <target>
rustup target add --toolchain <toolchain> <target>
rustup component list
rustup component add <component>
rustup component remove <component>
配置工具链安装位置
在系统环境变量中添加如下变量:
- CARGO_HOME - 指定cargo的安装目录
- RUSTUP_HOME - 指定rustup的安装目录
默认分别安装到用户目录下的.cargo
和.rustup
目录
配置国内镜像
在系统环境变量中添加如下变量(选一个就可以,可以组合):
RUSTUP_DIST_SERVER:https://mirrors.tuna.tsinghua.edu.cn/rustup
RUSTUP_UPDATE_ROOT:https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup
RUSTUP_DIST_SERVER:https://mirrors.ustc.edu.cn/rust-static
RUSTUP_UPDATE_ROOT:https://mirrors.ustc.edu.cn/rust-static/rustup
配置 cargo 国内镜像
在 cargo 安装目录下新建config
文件(注意 config 没有任何后缀),文件内容如下:
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'tuna'
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/crates.io-index"
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"
使用Rust模块系统管理代码
Rust 提供了一系列功能,可帮助你管理和组织代码。 这些功能称为“Rust 模块系统”。 系统由 Crate、模块、路径和工具组成,以与那些项结合使用
- 箱:Rust 箱是一个编译单元。 它是 Rust 编译器可以运行的最小代码段。 箱中的代码一起编译以创建二进制可执行文件或库。 在 Rust 中,仅将箱编译为可重复使用的单元。 箱包含具有隐式未命名顶级模块的 Rust 模块的层次结构
- 模块:Rust 模块通过让你管理箱内单个代码项的范围来帮助你组织程序。 结合使用的相关代码项或项可以分组到相同模块中。 递归代码定义可以跨越其他模块
- 路径:在 Rust 中,可以使用路径来命名代码中的项。 例如,路径可以是一个数据定义(例如,矢量、代码函数,甚至是模块)。 模块功能还可帮助你控制路径的隐私。 可以指定可公开访问的代码部分和私有部分。 通过该功能可以隐藏实现详细信息
使用Rust箱和库
- Rust 标准库。 在 Rust 练习中,你将会注意到以下模块:
- std::collections - 集合类型的定义,如 HashMap
- std::env - 用于处理环境的函数
- std::fmt - 控制输出格式的功能
- std::fs - 用于处理文件系统的功能
- std::io - 用于处理输入/输出的定义和功能
- std::path - 支持处理文件系统路径数据的定义和功能
- structopt - 用于轻松分析命令行参数的第三方箱
- chrono - 用于处理日期和时间数据的第三方箱
- regex - 用于处理正则表达式的第三方箱
- serde - 适用于 Rust 数据结构的序列化和反序列化操作的第三方箱
使用Cargo创建和管理项目
- 使用 cargo new 命令创建新的项目模板
- 使用 cargo build 编译项目
- 使用 cargo run 命令编译并运行项目
- 使用 cargo test 命令测试项目
- 使用 cargo check 命令检查项目类型
- 使用 cargo doc 命令编译项目的文档
- 使用 cargo publish 命令将库发布到 crates.io
- 通过将箱的名称添加到 Cargo.toml 文件来将依赖箱添加到项目
结构
Rust支持三种结构类型:经典结构、元组结构和单元结构。这些结构类型支持使用各种方式对数据进行分组和处理
- “经典C结构”最为常用。结构中的每个字段都具有名称和数据类型。定义经典结构后,可以使用语法.访问结构中的字段
- 元组结构类似于经典结构,但字段没有名称。要访问元组结构中的字段,请使用索引元组时所用的语法:.。与元组一样,元组结构中的索引值从0开始
- “单元结构”最常用作标记
struct Student {
name: String,
level: u8,
remote: bool
}
struct Grades(char, char, u8, f32);
struct Unit;
创建和使用数组
定义数组
- 未指定长度的逗号分隔的值列表
- 初始值后跟一个分号,然后是数组长度
// Declare array, initialize all values, compiler infers length = 7
let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
// Declare array, initialize all values to 0, length = 5
let bytes = [0; 5];
在编译时,数组的签名定义为 [T; size]
:
T
是数组中所有元素的数据类型
size
是表示数组长度的非负整数
该签名揭示有关数组的两个重要特征:
- 数组的每个元素都具有相同的数据类型。 数据类型永远不会更改
- 数组大小是固定的。 长度永远不会更改
仅数组中元素的值可随时间而更改。 数据类型和元素数量(长度)均保持不变。 只有这些值可以更改
读取数组的值
数组中的元素从 0 开始隐式编号。 我们在表达式 <array>[<index>]
中使用索引来访问的数组中的元素。 例如,my_array[0]
访问 my_array
变量中索引 0 位置的元素。 该表达式返回该索引位置的数组元素的值
// Days of the week
let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
// Get the first day of the week
let first = days[0];
// Get the second day of the week
let second = days[1];
Vector
Vector和数组一样,可以使用索引访问元素。mut
可变Vector添加元素可以使用 push
函数,删除Vector最后一个元素可以使用 pop
函数
if else
Rust中的if-else
语句块是可以充当表达式的
let formal = true;
let greeting = if formal { // if used here as an expression
"Good day to you." // return a String
} else {
"Hey!" // return a String
};
println!("{}", greeting) // prints "Good day to you."
合并多个测试条件
可以将if
和else
组合在一起形成else if
表达式。可以在开头的if
条件后面和else
条件前面使用多个else if
条件,这些条件是可选的
如果条件表达式的计算结果为true
,则执行相应的操作块,跳过后面任何的else if
和else
块。如果条件表达式的结果为false
,则跳过相应的操作块。如果if
和 else if
条件的计算结果都是false
,则执行所有的else
程序块
let num = 500; // num variable can be set at some point in the program
let out_of_range: bool;
if num < 0 {
out_of_range = true;
} else if num == 0 {
out_of_range = true;
} else if num > 512 {
out_of_range = true;
} else {
out_of_range = false;
}
错误处理
- 处理无法恢复的错误可以使用
panic!
- 如果值是可选或缺少值不是一种错误的情况,使用
Option
枚举
- 当可能出现问题并且调用方法必须处理问题时,使用
Result
枚举
使用Option类型来处理缺失
Option<T>
将自身列为两个变体之一:
enum Option<T> {
None, // The value doesn`t exist
Some(T), // The value exists
}
Option<T>
枚举声明的<T>
部分声明类型T
是通用的,将与Option
枚举的Some
变体相关联
None
和Some
不是类型,而是Option<T>
类型的变体。这表示在其他功能中,函数不能使用Some
或None
作为参数,而只能使用Option<T>
作为参数
如何才能访问到Some(data)
变体中的数据呢?
模式匹配
Rust 中提供了一个功能强大的运算符,称为 match
。 可利用该运算符,通过提供模式来控制程序流。 当 match 找到匹配的模式时,它会运行随该模式一起提供的代码
let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
for &index in [0, 2, 99].iter() {
match fruits.get(index) {
Some(fruit_name) => println!("It's a delicious {}!", fruit_name),
None => println!("There is no fruit! :("),
}
}
if let 表达式
let a_number: Option<u8> = Some(7);
match a_number {
Some(7) => println!("That's my lucky number!"),
_ => {},
}
这种情况下,可以用所有其他模式之后添加_
通配符,以匹配任何其他模式
使用if let
压缩代码:
let a_number: Option<u8> = Some(7);
if let Some(7) = a_number {
println!("That's my lucky number!");
}
使用unwrap
和expect
可以使用unwrap
和expect
方法直接访问Option
类型的内部值。但是要小心,因为如果变体是None
,则此方法会panic
使用Result 类型来处理错误
Rust提供了用于返回和传播错误的Result<T, E>
枚举。按照惯例,Ok(T)
变量表示成功并包含一个值,而变量Err(E)
表示错误并包含一个错误值
enum Result<T, E> {
Ok(T): // A value T was obtained.
Err(E): // An error of type E was encountered instead.
}
不同于描述缺少某个值的可能性Option
类型,Result
类型最适合在可能会失败的时候使用
Result
类型还具有unwrap
和 expect
方法,这些方法执行以下操作之一:
- 返回
Ok
变量中的值
- 如果变体是
Err
,则导致程序panic
#[derive(Debug)]
struct DivisionByZeroError;
fn safe_division(dividend: f64, divisor: f64) -> Result<f64, DivisionByZeroError> {
if divisor == 0.0 {
Err(DivisionByZeroError)
} else {
Ok(dividend / divisor)
}
}
fn main() {
println!("{:?}", safe_division(9.0, 3.0));
println!("{:?}", safe_division(4.0, 0.0));
println!("{:?}", safe_division(0.0, 2.0));
}
内存管理
Rust最强大的两个功能:所有权和借用
什么是所有权?
Rust包含用于管理内存的所有权系统。在编译时,所有权系统会检查一组规则,以确保所有权功能允许程序运行而不减慢速度
作用域界定规则
在Rust中,与大多数编程语言一样,变量仅在特定的作用域内有效。在Rust中,作用域常常由大括号{}
表示。常见的作用域还包括函数体、if
、else
和match
分支
在Rust中,“变量”通常称为“绑定”。这是因为Rust中的“变量”不是多变的,它们默认不可变,因此不会经常改变。相反,我们常常会想到与数据“绑定”的名称,所以称为“绑定”
假设一个string
变量,它是在某个作用域内定义的一个字符串:
// `string` is not valid and cannot be used here, because it's not yet declared.
{
let string = String::from("ferris"); // `string` is valid from this point forward.
// do stuff with `string`.
}
// this scope is now over, so `string` is no longer valid and cannot be used.
如果尝试在string
的范围之外使用它,将会得到一下错误示例:
{
let string = String::from("ferris");
}
println!("{}", string);
error[E0425]: cannot find value `string` in this scope
--> src/main.rs:5:20
|
5 | println!("{}", string);
| ^^^^^^ not found in this scope
所有权和删除
Rust给范围的概念增加了一个转折。当对象超出范围时,便会将其“删除”。删除变量会释放与其关联的所有资源。对于文件的变量,文件最终会被关闭。对于已分配了与其关联的内存的变量,内存将被释放
在Rust中,将绑定被删除时释放的内容与自己“关联”到一起的绑定将“拥有”这些内容
在上一个例子中,string
变量拥有与之关联的String
数据。string
本身拥有堆分配的内存,其中包含该字符串的字符。在作用域的末尾,string
被“删除”,它拥有的String
被删除,最后String
拥有的内存被释放
{
let string = String::from("ferris");
// string dropped here. The String data memory will be freed here.
}
移动语义
有时,我们不希望在作用域末尾删除与变量的关联的内容。相反,我们希望将某个项的所有权从一个绑定转移到另一个绑定
{
let string = String::from("ferris");
// transfer ownership of string to the variable ferris.
let ferris = string;
// ferris dropped here. The string data memory will be freed here.
}
需要了解一个关键问题,那就是所有权一旦转移,旧变量将不再有效。当我们将String
的所有权从 string
转移到 ferris
之后,将无法再使用string
变量
如果我们尝试在将String
从string
移动到ferris
之后使用string
,编译器将不会编译我们的代码:
let string = String::from("hello world");
let ferris = string;
// We'll try to use string after we've moved ownership of the string data from string to ferris.
println!("{}", string);
error[E0382]: borrow of moved value: `string`
--> src/main.rs:4:20
|
2 | let string = String::from("ferris");
| ------ move occurs because `string` has type `String`, which does not implement the `Copy` trait
3 | let ferris = string;
| ------ value moved here
4 | println!("{}", string);
| ^^^^^^ value borrowed here after move
该结果被称为“移动后使用”编译错误
在Rust中,一个项一次只能拥有一段数据
函数中的所有权
例子:将字符创作为参数传递给函数。将某个内容作为参数传递给函数,会将该内容移动到函数中
fn process(input: String) {}
fn caller() {
let s = String::from("hello world");
process(s); // Ownership of the string in `s` moved into `process`
process(s); // Error! ownership already moved.
}
编译器提醒s
已被移动
error[E0382]: use of moved value: `s`
--> src/main.rs:6:13
|
4 | let s = String::from("Hello, world!");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
5 | process(s); // Transfers ownership of `s` to `process`
| - value moved here
6 | process(s); // Error! ownership already transferred.
| ^ value used here after move
复制而不是移动
fn process(input: u32) {}
fn caller() {
let n = 1u32;
process(n); // Ownership of the number in `n` copied into `process`
process(n); // `n` can be used again because it wasn`t moved, it was copied
}
简单类型,如数字是“复制”类型。它们实现Copy
特征,这意味着它们被复制而不是移动。大多数简单类型都执行相同的操作。复制数字的成本低,因此复制这些值是有意义的。复制字符串、向量或其他复杂类型的成本可能高昂,因此它们没有实现Copy
特征,而是被移动
复制不实现Copy
的类型
解决上一个示例中所示错误的一种方法是:在移动类型之前,显式复制它们,这在Rust中称为克隆。调用.clone
会复制内存并生成一个新值。新值被移动,这意味着仍然可以使用旧值
fn process(s: String) {}
fn caller() {
let s = String::from("Hello world");
process(s.clone()); // Passing another value, cloned from `s`
process(s); // s was never moved and so it can still be used.
}
这种方法可能有用,但会导致代码运行速度变慢,因此每次调用clone
都是对数据的一次完整复制。此方法通过包括内存分配或其他高成本的操作。可以使用引用来“借用”值,从而避免这些成本
了解借用
通过将所有权从一个变量转移到另一个变量来转移一个值的所有权。对于实现Copy
特征的类型,例如简单的值(如数字),所有权不能被转移
还可以使用克隆过程显式复制值。调用clone
方法并获取复制的新值,这样原始值未被移动且仍可使用
此类功能通过使用引用来提供,通过引用,可以“借用”一些值,而无需拥有它们
let greeting = String::from("hello");
let greeting_reference = &greeting; // We borrow `greeting` but the string data is still owned by `greeting`
println!("Greeting: {}", greeting); // We can still use `greeting`
greeting
是使用引用符号(&
) 借用的。变量greeting_reference
的类型为字符串引用(&String
)。由于只借用了greeting
,并没有移动所有权,因此,在创建greeting_reference
之后仍然可以使用greeting
函数中的引用
fn print_greeting(message: &String) {
println!("Greeting: {}", message);
}
fn main() {
let greeting = String::from("Hello");
print_greeting(&greeting); // `print_greeting` takes a `&String` not an owned `String` so we borrow `greeting`
print_greeting(&greeting); // Since `greeting` didn't move into `print_greeting` we can use it again
}
通过借用,无需完全拥有某个值即可使用它。不过,借用值意味着并不能像完全拥有值那样执行权限范围内的所有操作
改变借用的值
fn change(message: &String) {
message.push_str("!"); // We try to add a "!" to the end of our message
}
fn main() {
let greeting = String::from("Hello");
change(&greeting);
}
代码不能通过编译。收到的错误是:
error[E0596]: cannot borrow `*message` as mutable, as it is behind a `&` reference
--> src/main.rs:2:3
|
1 | fn change(message: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
2 | message.push_str("!"); // We try to add a "!" to the end of our message
| ^^^^^^^ `message` is a `&` reference, so the data it refers to cannot be borrowed as mutable
借用和可变引用
不可变引用和可变引用还有一个不同之处:会对我们生成Rust程序的方式有根本的影响。借用任何T
类型的值时,以下规则都适用:
代码必须同时实现以下任一定义,但不能同时实现这两个定义:
- 一个或多个不可变引用(
&T
)
- 恰好一个可变引用(
&mut T
)
使用生命周期验证引用
fn main() {
let x;
{
let y = 42;
x = &y; // We store a reference to `y` in `x` but `y` is about to be dropped.
}
println!("x: {}", x); // `x` refers to `y` but `y has been dropped!
}
这段代码编译失败
fn main() {
let x; // ---------+-- 'a
{ // |
let y = 42; // -+-- 'b |
x = &y; // | |
} // -+ |
println!("x: {}", x); // |
}
内层 'b
块的生存期比外层 'a
块的生存期更短
Rust 编译器可以使用借用检查器来验证借用是否有效。 借用检查器会在编译时比较两个生存期。 在此场景中,x
的生存期为 'a
,但它引用了生存期为 'b
的值。 引用主体(生存期为 'b
的 y)的生存期比引用(生存期为 'a
的 x
)的生存期短,因此程序不会进行编译
另一个例子:
fn main() {
let magic1 = String::from("abracadabra!");
let result;
{
let magic2 = String::from("shazam!");
result = longest_word(&magic1, &magic2);
}
println!("The longest magic word is {}", result);
}
fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
if x.len() > y.len() {
x
} else {
y
}
}
错误:
error[E0597]: `magic2` does not live long enough
--> src/main.rs:6:40
|
6 | result = longest_word(&magic1, &magic2);
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `magic2` dropped here while still borrowed
8 | println!("The longest magic word is {}", result);
| ------ borrow later used here
此错误表明编译器预期 magic2
的生存期与返回值和 x
输入参数的生存期相同。 Rust 预料会出现这种行为,因为我们使用同一生存期名称 ('a
) 对函数参数和返回值的生存期进行了批注
如果我们检查代码,作为人类,我们会看到 magic1
比 magic2
长。 我们会看到,结果包含对 magic1
的引用,该引用的生存期足够长,因此有效。 但是,Rust 在编译时无法运行该代码。 它会将 &magic1
和 &magic2
引用都视为可能的返回值,并会发出我们此前看到的错误
longest_word
函数返回的引用生存期与传入的引用生存期中较小者相匹配。 因此,代码可能包含无效引用,借用检查器将禁止该引用
Trait
trait Area {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Area for Circle {
fn area(&self) -> f64 {
use std::f64::consts::PI;
PI * self.radius.powf(2.0)
}
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
为实现某种类型的特征,使用关键字impl Trait for Type
,其中Trait
是要实现的特征的名称,Type
是实现器结构体或枚举的名称
在impl
块中,放置特征所需的方法签名,并使用自己希望特征的方法对于特定类型具有的特定行为填充方法主体
迭代器
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterator
具有方法next
,调用时它将返回Option<Item>
。只要有元素,next
方法就会返回Some(Item)
。用尽所有元素后,它将会返回None
以指示迭代已完成
type Item
和 Self::Item
,它们使用此特征定义关联的类型。此定义意味着Iterator
特征的每一次实现还需要定义关联的Item
类型,该类型用作next
方法的返回类型。换句话说,Item
类型将是从for
循环块内的迭代器返回的类型
模块、包和第三方crate
理解代码组织背后的概念
- 包
- 包含一个或多个crete内的功能
- 包括有关如何生成这些crate的信息。该信息位于
Cargo.toml
文件中
- 箱
- 是编译单元,即 Rust 编译器可以运行的最小码量
- 编译完成后,系统将生成可执行文件或库文件
- 其中包含未命名的隐式顶层模块
- 模块
- 是箱内的代码组织单位(或为嵌套形式)
- 可以具有跨其他模块的递归定义
程序包
每当运行Cargo new <project-name>
命令时,Cargo将会为我们创建一个包
板条箱
Rust 的编译模型集中在名为箱 的项目中,可以将这些项目译为二进制文件或库文件
使用cargo new
命令创建的每个项目本身都是箱。可以在项目中用作依赖项的所有第三方Rust 代码也是单个箱
库文件箱
创建库命令cargo new --lib
模块
Rust 具有功能强大的模块系统。该系统可以分层方式将代码拆分为逻辑单元,从而提高其可读性和重用性
模块是项的集合:
- 常量
- 类型别名
- 函数
- 结构
- 枚举
- trait
impl
块
- 其他模块
模块还控制项隐私。项隐私将项标识为 public 和 private。public 表示项可以由外部代码使用。private 表示该项是内部实现详细信息,不能供外部使用
mod math {
type Complex = (f64, f64);
pub fn sin(f: f64) -> f64 { /* ... */ }
pub fn cos(f: f64) -> f64 { /* ... */ }
pub fn tan(f: f64) -> f64 { /* ... */ }
}
println!("{}", math::cos(45.0));
如果源文件中存在mod
声明,则在运行编译器之前,系统会将模块文件的内容插入到mod
声明的源文件中的所在位置。换句话说,系统不会对模块进行单独编译,只会编译箱
Rust 编译器会检查以确定是否可以跨模块使用项。默认情况下,Rust 中的所有内容都是专用的,并且只能由当前模块及其后代访问。与此相反,当项被声明pub
时,则可以将其视为可供外界访问
// Declare a private struct
struct Foo;
// Declare a public struct with a private field
pub struct Bar {
field: i32,
}
// Declare a public enum with two public variants
pub enum State {
PubliclyAccessibleVariant,
PubliclyAccessibleVariant2,
}
向项目中添加第三方箱
[dependencies]
regex = "1.4.2"
如果在Cargo.toml
没有[dependencies]
部分,手动添加该部分即可
单元测试
Rust中的单元测试是用#[test]
属性标记的简单函数,可用于验证非测试代码是否按照预期方式正常运行。系统仅会在测试代码时编译这些函数
测试函数会运行要测试的代码。然后,这些函数通常使用assert!
或assert_eq!
宏来检查结果
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[test]
fn add_works() {
assert_eq!(add(1, 2), 3);
assert_eq!(add(10, 12), 22);
assert_eq!(add(5, -2), 3);
}
预期的失败
使用should_panic
,便可以检查panic!
。如果将此属性添加到测试函数,则当函数中的代码崩溃时,测试便会通过。当代码不崩溃时,测试便会失败
#[test]
#[should_panic]
fn add_fails() {
assert_eq!(add(2, 2), 7);
}
忽略测试
可以使用#[ignore]
属性对带有#[test]
属性批注的函数进行批注。此属性会令系统在测试过程中跳过该测试函数
#[test]
#[ignore = "not yet reviewed by the Q.A. team"]
fn add_negatives() {
assert_eq!(add(-2, -2), -4)
}
测试模块
大多数单元测试将进入带有#[cfg(test)]
属性的字模块
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod add_function_tests {
use super::*;
#[test]
fn add_works() {
assert_eq!(add(1, 2), 3);
assert_eq!(add(10, 12), 22);
assert_eq!(add(5, -2), 3);
}
#[test]
#[should_panic]
fn add_fails() {
assert_eq!(add(2, 2), 7);
}
#[test]
#[ignore]
fn add_negatives() {
assert_eq!(add(-2, -2), -4)
}
}
cfg
属性负责控制条件编译,并仅会在谓词为true
时编译其所附带的内容。每当执行cargo test
命令时,Cargo都会自动发出test
编译标志,因此,当我们运行测试时,该标志将会始终为true
use super::*;
声明是add_function_tests
模块内部代码访问外部模块中add
的必要条件
写入文档测试
通过Rust,可以将文档示例作为测试来执行。记录Rust库的主要方式是使用三斜杠///
注释源代码,即熟知的文档注释。文档注释会写入到Markdown中,并支持其中的代码块,因此可以对这些代码块进行编译并将其用作测试
若要尝试此功能,需要先创建一个新的库项目
cargo new --lib r20-test-doc
/// Generally, the first line is a brief summary describing the function.
///
/// The next lines present detailed documentation.
/// Code blocks start with triple backticks. The code has an implicit `fn main()` inside and `extern crate <cratename>`,
/// which means you can just start writing code.
///
/// ```
/// let result = basic_math::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
与单元测试一样,如果文档测试未在运行时崩溃,则表示其顺利通过。若要验证某些结果,使用aeert!
宏来验证实际输出是否与预期相同。可以通过命令cargo test
调用此代码的测试套件
写入集成测试
单元和文档测试提供了简洁具体的测试。 但是,将 Crate 作为一个整体测试也是一种好办法。 然后,我们可以确认 Crate 的各代码部分是否按预期一起运行
若要将 Crate 作为成体进行测试,可以使用集成测试。 Rust 测试套件支持这种类型的测试,该测试仅调用库的公共 API 包含的函数。 我们可以使用集成测试来检查代码在其他人员使用它时的工作情况
这些测试的独特之处在于它们存在于单独的目录和文件中,因此它们可以在外部对库代码进行测试。 使用 Cargo 运行集成测试时,请将测试放在“tests”目录中。 Cargo 会运行此目录中的每个源文件。 在项目目录中创建测试,级别与你的 src 目录相同
闭包
三种Fn
特征
FnOnce
,该类型的闭包会拿走被捕获变量的所有权
fn fn_once<F>(func: F)
where
F: FnOnce(usize) -> bool + Copy,
{
println!("{func(3)}");
println!("{func(4)}");
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z| {z == x.len()})
}
如果想强制闭包取得捕获变量的所有权,可以在参数列表前添加move
关键字,这种用法通常用于闭包的生命周期大于捕获变量的生命周期,例如将闭包返回或移入其他线程
use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println("Here's a vector: {:?}", v);
});
handle.join().unwrap();
FnMut
,它以可变借用的方式捕获了环境中的值,因此可以修改该值
pub fn fn_mut() {
let mut s = String::new();
let mut update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}", s);
}
pub fn fn_mut1() {
let mut s = String::new();
let update_string = |str| s.push_str(str);
exec(update_string);
println!("{:?}", s);
}
fn exec<'a, F: FnMut(&'a str)>(mut f: F) {
f("world")
}
pub fn fn_1() {
let s = String::from("hello");
let update_string = |chs| println!("{} {}", s, chs);
exec1(update_string);
}
fn exec1<F: Fn(String)>(f: F) {
f("world".to_string())
}
move
和Fn
在上面,我们讲到了move
关键字对于FnOnce
特征的重要性,但实际上使用了move
的闭包依然可能实现了Fn
或FnMut
特征
因此,一个闭包实现了哪种Fn特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们.move本身强调的就是后者,闭包如何捕获变量:
fn main() {
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
fn exec<F: FnOnce()>(f: F) {
f()
}
这个闭包中使用了move关键字,所以我们的闭包捕获了它,但是由于闭包对s的使用仅仅是不可变借用,因此该闭包实际上还实现了Fn特征.因为该闭包不仅仅实现了FnOnce特征
fn main() {
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
fn exec<F: Fn()>(f: F) {
f()
}
三种Fn的关系
实际上,一个闭包并不仅仅实现某一种Fn特征,规则如下:
- 所有的闭包都自动实现了FnOnce特征,因此任何一个闭包都至少可以被调用一次
- 没有移出所捕获变量的所有权的闭包自动实现了FnMut特征
- 不需要对捕获变量进行改变的闭包自动实现了Fn特征
pub fn fn_all() {
let s = String::from("Hello");
let update_string = || println!("{}", s);
exec_fn(update_string);
exec_fn_once(update_string);
exec_fn_mut(update_string);
}
fn exec_fn_once<F: FnOnce()>(f: F) {
f()
}
fn exec_fn_mut<F: FnMut()>(mut f: F) {
f()
}
fn exec_fn<F: Fn()>(f: F) {
f()
}
闭包作为函数返回值
需要加上impl
,或者使用智能指针(如Box)包裹返回
fn factory(x:i32) -> impl Fn(i32) -> i32 {
let num = 5;
if x > 1{
move |x| x + num
} else {
move |x| x - num
}
}
fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
let num = 5;
if x > 1{
move |x| x + num
} else {
move |x| x - num
}
}
迭代器
Iterator 和 IntoIterator的区别
这两个其实很容易搞混.
Iterator
就是迭代器特征,只有实现了它才能成为迭代器,才能调用next
IntoIterator
强调的是某一个类型如果实现了该特征,它可以通过into_iter
, iter
等方法变成一个迭代器
消费者与适配器
消费者是迭代器上的方法,它会消费掉迭代器中的元素,然后返回其类型的值,这些消费者都是有一个共同的特点:在它们的定义中,都依赖next
方法来消费元素,因此这也是为什么迭代器要实现Iterator
特征,而该特征必须要实现next
方法的原因
消费者适配器
只要迭代器上的某个方法A在其内部调用了next
方法,那么A就被称为消费性适配器
:因为next
方法会消耗掉迭代器上的元素,所以方法A的调用也会消耗掉迭代器上的元素
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
println!("{:?}",v1);
}
如代码注释中所说明的:在使用sum
方法后,我们将无法再使用v1_iter
,因为sum
拿走了该迭代器的所有权:
fn sum<S>(self) -> S
where
Self: Sized,
S: Sum<Self::Item>,
{
Sum::sum(self)
}
迭代器适配器
既然消费者适配器是消费掉迭代器,然后返回一个值.那么迭代器适配器,顾名思义,会返回一个新的迭代器,这是实现链式方式方法调用的关键: v.iter().map().filter()...
与消费者适配器不同,迭代器适配器是惰性的,意味着需要一个消费者适配器来收尾,最终将迭代器转换成一个具体的值:
let v1 = Vec<u32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: iterators are lazy and do nothing unless consumed
这里的map
方法是一个迭代器适配器,它是惰性的,不产生任何行为,因此我们还需要一个消费者适配器进行收尾:
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
collect
上面代码中, 使用了collect
方法,该方法就是一个消费者适配器, 使用它可以将一个迭代器中的元素收集到指定类型中, 这里我们为v2标注了Vec<_>
类型,就是为了告诉collect
: 请把迭代器中的元素消费掉, 然后把值收集成Vec<_>
类型, 至于为何使用_
, 因为编译器会帮我们自动推导
智能指针
堆栈
栈内存从高位地址向下增长,且栈内存是连续分配的
在 Rust 中,main 线程的栈大小是 8MB,普通线程是 2MB,在函数调用时会在其中创建一个临时栈空间,调用结束后 Rust 会让这个栈空间里的对象自动进入 Drop 流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的
与栈相反,堆上内存则是从低位地址向上增长,堆内存通常只受物理内存限制,而且通常是不连续的,因此从性能的角度看,栈往往比堆更高
相比其它语言,Rust 堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可),例如以下代码:
fn main() {
let b = foo("world");
println!("{}", b);
}
fn foo(x: &str) -> String {
let a = "Hello, ".to_string() + x;
a
}
堆栈的性能
很多人可能会觉得栈的性能肯定比堆高,其实未必。 由于我们在后面的性能专题会专门讲解堆栈的性能问题,因此这里就大概给出结论:
- 小型数据,在栈上的分配性能和读取性能都要比堆上高
- 中型数据,栈上分配性能高,但是读取性能和堆上并无区别,因为无法利用寄存器或 CPU 高速缓存,最终还是要经过一次内存寻址
- 大型数据,只建议在堆上分配和使用
Box 的使用场景
由于 Box 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此 Box 相比其它智能指针,功能较为单一,可以在以下场景中使用它:
- 特意的将数据分配在堆上
- 数据较大时,又不想在转移所有权时进行数据拷贝
- 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时
- 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型
Drop
互斥的Copy和Drop
我们无法为一个类型同时实现Copy
和Drop
特征. 因为实现了Copy
的特征会被编译器隐式的复制, 因此非常难以预测析构函数执行的时间和频率. 因此这些实现了Copy
的类型无法拥有析构函数
#[derice(Debug)]
struct Foo;
impl Drop for Foo {
fn drop(&mut self) {
println!("Deopping Foo!");
}
}
以上代码报错如下:
error[E0184]: the trait `Copy` may not be implemented for this type; the type has a destructor
--> src/main.rs:24:10
|
24 |
| ^^^^ Copy not allowed on types with destructors
关于Rc的简单总结
- Rc/Arc 是不可变引用, 无法修改它指向的值, 只能进行读取, 如果要修改, 需要配合内部可变性RefCell 或互斥锁Mutex
- 一旦最后一个拥有者消失, 则资源会自动被回收, 这个生命周期是在编译器就确定下来的
- Rc 只能用于同一线程内部, 想要用于线程之间的对象共享, 需要使用Arc
- Rc 是一个智能指针, 实现了Deref 特征, 因此无需先解开Rc 指针, 再使用里面的T, 而是可以直接使用T
Arc的性能损耗
原子化可以带来线程安全, 但是都会伴随着性能损耗, 而且这种性能损耗还不小
Cell
Cell 和 RefCell 在功能上没有区别,区别在于 Cell 适用于 T 实现 Copy 的情况:
use std::cell::Cell;
fn main() {
let c = Cell::new("asdf");
let one = c.get();
c.set("qwer");
let two = c.get();
println!("{},{}", one, two);
}
注意:
Cell
智能指针跟上面笔记的智能指针有点不太一样的用法, 如例子所示, 获取指针的值是使用get
, 设置指针的值是使用set
而且获取到值之后还可以设置值? 这个貌似是违背了Rust的借用规则? (应该是的(bushi))
通过Cell::from_mut解决借用冲突
在Rust1.37版本中新增了两个非常使用的方法:
- Cell::from_mut,该方法将 &mut T 转为 &Cell
- Cell::as_slice_of_cells,该方法将 &Cell<[T]> 转为 &[Cell]
fn is_even(i: i32) -> bool {
i % 2 == 0
}
fn retain_even(nums: &mut Vec<i32>) {
let mut i = 0;
for num in nums.iter().filter(|&num| is_even(*num)) {
nums[i] = *num;
i += 1;
}
nums.truncate(i);
}
fn retain_even(nums: &mut Vec<i32>) {
let mut i = 0;
for j in 0..nums.len() {
if is_even(nums[j]) {
nums[i] = nums[j];
i += 1;
}
}
nums.truncate(i);
}
use std::cell::Cell;
fn retain_even(nums: &mut Vec<i32>) {
let slice: &[Cell<i32>] = Cell::from_mut(&mut nums[..])
.as_slice_of_cells();
let mut i = 0;
for num in slice.iter().filter(|num| is_even(num.get())) {
slice[i].set(num.get());
i += 1;
}
nums.truncate(i);
}
RefCell
由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的
所有权, 借用规则与智能指针的对比:
Rust规则 |
智能指针带来的额外规则 |
一个数据只有一个所有者 |
Rc/Arc 让一个数据可以拥有多个所有者 |
要么多个不可变借用, 要么一个可变借用 |
RefCell 实现编译器可变, 不可变引用共存 |
违背规则导致编译错误 |
违背规则导致运行时panic |
可以看出, Rc/Arc
和RefCell
合在一起, 解决了Rust中严苛的所有权和借用规则带来的某些场景下难使用的问题. 但是它们并不是银弹, 例如RefCell
实际上并没有解决可变引用和不可变引用共存的问题, 只是将报错从编译期推迟到运行时, 从编译器错误变成了panic
异常:
use std::cell::RefCell;
fn main() {
let s = RefCell::new(String::from("hello, world"));
let s1 = s.borrow();
let s2 = s.borrow_mut();
println!("{},{}", s1, s2);
}
thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:6:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
但是依然会因为违背了借用规则导致了运行期panic
当确信编译器误报但不知道如何解决时, 或者有一个引用类型, 需要被四处使用和修改然后导致借用关系难以管理时, 可以优先考虑使用RefCell
RefCell简单总结
- 与Cell用于可Copy的值不同, RefCell用于引用
- RefCell只是将借用规则从编译期推迟到程序运行期, 并不能帮你绕过这个规则
- RefCell适用于编译期误报或者一个引用被在多处代码使用, 修改以至于难于管理借用关系时
- 使用RefCell时, 违背借用规则会导致运行期的panic
选择Cell还是RefCell
- Cell只适用于Copy类型, 用于提供值, 而RefCell用于提供引用
- Cell不会panic, 而RefCell会
Rc + RefCell组合使用
在Rust中, 一个常见的组合就是Rc 和RefCell 在一起使用, 前者可以实现一个数据拥有多个所有者, 后者可以实现数据的可变性:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string()));
let s1 = s.clone();
let s2 = s.clone();
s2.borrow_mut().push_str(", oh yeah!");
println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}
CPU损耗
- 对 Rc 解引用是免费的(编译期),但是 * 带来的间接取值并不免费
- 克隆 Rc 需要将当前的引用计数跟 0 和 usize::Max 进行一次比较,然后将计数值加 1
- 释放(drop) Rc 需要将计数值减 1, 然后跟 0 进行一次比较
- 对 RefCell 进行不可变借用,需要将 isize 类型的借用计数加 1,然后跟 0 进行比较
- 对 RefCell 的不可变借用进行释放,需要将 isize 减 1
- 对 RefCell 的可变借用大致流程跟上面差不多,但是需要先跟 0 比较,然后再减 1
- 对 RefCell 的可变借用进行释放,需要将 isize 加 1
多线程并发编程
CPU多核
并行一定是并发, 反之并发只有在多核时才可能并行
单核心并发
关键在于: 快速轮换处理不同的任务, 给用户带来所有任务同时在运行的假象
多核心并行
当CPU核心增多到N时, 那么同一时间就能有N个任务被处理, 那么我们的并行度就是N, 相应的处理效率也就变成了单核心的N倍(实际情况并没有这么高)
多核心并发
当核心增多到N时, 操作系统同时进行的任务肯定远不止N个, 这些任务将被放入M个线程队列中, 接着交给N个CPU核心去执行, 最后实现了M:N的处理模型, 在这种情况下, 并发与并行是同时发生的, 所有用户任务从表面来看都是并发的运行, 但实际上, 同一时刻只有N个任务能被同时并行的处理
- 如果某个系统支持两个或多个动作的
同时存在
, 那么这个系统就是一个并发系统
- 如果某个系统支持两个或多个动作的
同时执行
, 那么这个系统就是一个并行系统
在并发程序中可以同时拥有两个或多个线程. 这意味着, 如果程序在单核处理器上运行, 那么这两个线程将交替地换入或换出内存. 这些线程是同时"存在"
的 – 每个线程都是处于执行过程中的某个状态. 如果程序能够并行执行, 那么就一定是运行在多核处理器上. 此时, 程序中的每个线程都将分配到一个独立的处理器核心上, 因此可以同时运行
所以, "并行"概念是"并发"概念的一个子集
. 也就是说, 编写一个拥有多线程或者进程的并发程序, 但如果没有多核处理器来执行这个程序, 那么就不能以并行的方式运行代码. 因此, 凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为, 都属于并发编程的范畴
编程语言的并发模型
- 由于操作系统提供了创建线程的API, 因此部分语言会直接调用该API来创建线程, 因此最终程序内的线程和该程序占用的操作系统线程相等, 一般称之为1:1线程模型, 例如Rust
- 还有些语言的内部实现了自己的线程模型(绿色线程, 协程), 程序内部的M个线程最后会以某种隐射方式使用N个操作系统线程去运行, 因此称之为M:N现成模型, 其中M和N并没有特定的彼此限制关系. 一个典型的代表就是Go语言
- 还有写语言使用了Actor模型, 基于消息传递进行并行, 例如Erlang语言
绿色线程/协程的视线会显著增大运行时的大小, 因此Rust只在标准库中提供了1:1的线程模型, 如果你愿意牺牲一些性能来换取更精确的线程控制以及更小的线程上下文切换成本, 那么可以使用Rust中的M:N模型, 这些模型由三方库提供了实现, 例如大名鼎鼎的tokio
使用线程
由于多线程的代码是同时运行的,因此我们无法保证线程间的执行顺序,这会导致一些问题:
- 竞态条件(race conditions),多个线程以非一致性的顺序同时访问数据资源
- 死锁(deadlocks),两个线程都想使用某个资源,但是又都在等待对方释放资源后才能使用,结果最终都无法继续执行
- 一些因为多线程导致的很隐晦的 BUG,难以复现和解决
多线程的性能
创建线程的性能
据不精确估算, 创建一个线程大概需要0.24毫秒, 随着线程的变多, 这个值会变得更大, 因此线程的创建耗时是不可忽略的, 只有当真的需要处理一个值得用线程去处理的任务时, 才使用线程, 一些鸡毛蒜皮的任务, 就无需创建线程了
创建多少线程合适
因为CPU的核心数限制, 当任务是CPU密集型时, 就算线程数超过CPU核心数, 也并不能帮你获得更好的性能, 因为每个线程的任务都可以轻松让CPU的某个核心跑满, 既然如此, 让线程数等于CPU核心数是最好的
但是当你的任务大部分时间都处于阻塞状态时, 就可以考虑增加多线程数量, 这样当某个线程处于阻塞状态时, 会被切走, 进而运行其它的线程, 典型就是网络IO操作, 我们可以为每一个进来的用户连接创建一个线程去处理, 该连接绝大部分时间都是处于IO读取阻塞状态, 因此有限的CPU核心完全可以处理成百上千的用户连接线程, 但是事实上, 对于这种网络IO情况, 一般都不再使用多线程的方式了, 毕竟操作系统的线程数是有限的, 意味着并发数也很容易达到上限, 而且过多的线程也会导致线程上下文切换的代价过大, 使用async/await
的M:N
并发模型, 就没有这个烦恼
总结
Rust 的线程模式是1:1模型, 因为Rust要保持尽量小的运行时
使用thread::spawn来创建线程, 创建出的多个线程之间并不存在执行顺序关系, 因此代码逻辑千万不要依赖于线程间的执行顺序
main 线程若是结束, 则所有子线程都将被终止, 如果希望等待子线程结束后, 再结束main 线程, 需要使用创建线程时返回的句柄join 方法
在线程中无法直接借用外部环境中的变量值, 因为新线程的启动时间和结束时间点是不确定的, 所以Rust 无法保证该线程中借用的变量在使用过程中依然是合法的. 可以使用move 关键字将变量的所有权转移给新的线程来解决此问题
父线程结束后, 子线程仍在持续运行, 直到子线程的代码运行完成或者main 线程的结束
线程间的消息传递
多发送者, 单接受者
标准库提供了通道std::sync::mpsc
, 其中mpsc
是multiple producer, single consumer
的缩写, 代表了该通道支持多个发送者, 但是只支持唯一的接收者. 当然, 支持多个发送者也就意味着支持单个发送者
同步和异步的通道
Rust标准库的mpsc
通道其实氛围两种类型: 同步和异步
异步通道
上述的例子都是使用的异步通道: 无论接收者是否正在接收消息, 消息发送者在发送消息的时候都不会阻塞
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx)= mpsc::channel();
let handle = thread::spawn(move || {
println!("发送之前");
tx.send(1).unwrap();
println!("发送之后");
});
println!("睡眠之前");
thread::sleep(Duration::from_secs(3));
println!("睡眠之后");
println!("receive {}", rx.recv().unwrap());
handle.join().unwrap();
}
同步通道
与异步通道相反, 同步通道发送消息是阻塞的, 只有在消息被接收后才接触阻塞
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx)= mpsc::sync_channel(0);
let handle = thread::spawn(move || {
println!("发送之前");
tx.send(1).unwrap();
println!("发送之后");
});
println!("睡眠之前");
thread::sleep(Duration::from_secs(3));
println!("睡眠之后");
println!("receive {}", rx.recv().unwrap());
handle.join().unwrap();
}
传输多种类型的数据
可以使用枚举来实现
但是有一点需要注意, Rust会按照枚举中占用内存最大的那个成员进行内存对齐, 这意味着就算传输的是枚举占用内存最小的成员, 它占用的内存依然和最大的成员相同, 因此会造成内存上的浪费
线程同步: 锁, Condvar和信号量
共享内存可以说是同步的灵魂, 因为消息传递的底层实际上也是通过共享内存来实现, 两者区别:
- 共享内存相对消息传递能节省多次内存拷贝的成本
- 共享内存的实现简洁的多
- 共享内存的锁竞争更多
消息传递适用的场景很多:
- 需要可靠和简单的(简单不等于简洁)实现时
- 需要模拟现实世界, 例如用消息去通知某个目标执行相应的操作时
- 需要一个任务处理流水线(管道)时, 等等
而使用共享内存(并发原语)的场景往往就比较简单粗暴: 需要简洁的实现以及更高的性能时
总之, 消息传递类似一个单所有权的系统: 一个值同时只能有一个所有者, 如果另一个线程需要该值的所有权, 需要将所有权通过消息传递进行转移. 而共享内存类似于一个多所有权的系统: 多个线程可以同时访问同一个值
互斥锁Mutex
互斥锁Mutex(mutual exclusion)
Mutex让多个线程并发的访问同一个值变成了排队访问: 同一时间, 只允许一个线程A 访问该值, 其他线程需要等待A 访问完成之后才能继续
单线程中使用Mutex:
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("m = {:?}", m);
}
多线程中使用Mutex:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
内部可变性
Rc 和 RefCell的结合, 可以实现单线程的内部可变性
由于Mutex 可以支持修改内部数据, 当结合Arc 一起使用, 可以实现多线程的内部可变性
简单总结: Rc/RefCell 用于单线程内部可变性, Arc/Mutex 用于多线程内部可变性
需要小心使用Mutex
- 在使用数据前必须先获取锁
- 在数据使用完成后, 必须及时的释放锁, 比如文章开头的例子, 使用内部语句块的目的就是为了及时释放锁
读写锁RwLock
Mutex 会对每次读写都进行加锁, 但某些时候, 我们需要大量的并发读, Mutex 就无法满足需求, 此时就可以使用RwLock
use std::sync::RwLock;
fn main() {
let lock = RwLock::new(5);
{
let r1 = lock.read().unwrap();
let r2 = lock.read().unwrap();
assert_eq!(*r1, 5);
assert_eq!(*r2, 5);
}
{
let mut w = lock.write().unwrap();
*w += 1;
assert_eq!(*w, 6);
}
}
- 同时允许多个读, 但是最多只能有一个写
- 读和写不能同时存在
- 读可以使用
read
try_read
, 写write
try_write
, 在实际项目中, try_xxx
会安全很多
Mutex还是RwLock
首先简单性上Mutex完胜, 因为使用RwLock得操心几个问题:
- 读和写不能同时发生, 如果使用try_xxx解决, 就必须做大量的错误处理和失败重试机制
- 当读多写少时, 写操作可能会因为一直无法获得锁导致连续失败多次(Write starvation)
- RwLock其实是操作系统提供的, 实现原理要比Mutex复杂得多, 因此单就锁的性能而言, 比不上原生实现的Mutex
再来简单总结下两者的使用场景:
- 追求高并发读取时, 使用RwLock, 因为Mutex一次只允许一个线程去读取
- 如果要保证写操作的成功性, 使用Mutex
- 不知道哪个适合, 统一使用Mutex