Rust 是一门高性能、 内存安全、并发友好的现代系统编程语言, 其设计目标是在不牺牲性能的前提下解决内存安全和并发问题,同时保持开发效率.
第一章
1. Hello world
rustc 编译 *.rs 文件生成*.exe可执行文件.
2.Cargo
cargo是rust的包管理器和构建工具, 跟随rust自动安装, 二者关系类似于nodejs与npm.
使用 cargo 创建项目
cargo new yourProjectName
cd ./yourProjectName
# 检查文件是否存在编译错误, 不生成可执行文件
cargo check
# 编译
cargo build
# 运行
cargo run
基本目录结构如下:
Cargo.toml类似于package.json管理项目配置以及依赖.
第二章 常见概念
变量和可变性
默认情况下, rust 变量是不可变的 (immutable), 即一旦一个值被绑定到一个变量后, 不能再改变该变量的值.
let vs const 的区别:
- Const 与 mut 不能共用, 而 let 可以
- Const 常量绑定的值始终都不能修改且必须进行类型注释
- Const 常量的初始值只能设置为常量表达式的值, 而不能设置为在运行时计算出来的结果值
- eg:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
- eg:
变量遮蔽 vs let mut
// 变量遮蔽: 更加方便的对同名变量的值进行转换并保持之后的不可变性, 且支持修改值的类型
let x = "some text";
let x = x.len(); // string to number
// let mut: 可变变量且不支持直接修改值的类型
let mut spaces = " ";
spaces = spaces.len(); // error type
数据类型
rust是强类型语言, 编译时所有变量必须有明确的数据类型.
-
scalar types (基础)
- integers, floating-point numbers, Booleans, and characters.
-
compound types (复合)
- tuples 元组: 组合多个不同类型值的元素, 且 tuple 长度固定不变
- arrays 数组: 组合多个相同类型的元素, 长度固定
- 标准库 vector : 与数组类似, 但长度可灵活变化
// tuple
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
// tuple 支持解构赋值
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
// 常见数组初始化
let a: [i32; 5] = [1, 2, 3, 4, 5]; // 指定类型和数量并初始化
let a = [3; 5]; // 等价 [3, 3, 3, 3, 3]
Functions
- statement语句, 执行某个操作且不返回任何值
- expression表达式, 以表达式的计算结果作为结果值
let y = 6; // statement
x + 1; // expression
fn five() -> i32 {
5 // 返回值
}
fn plus_one(x: i32) -> i32 {
x + 1 // 作为表达式, 且其值作为函数返回值与 i32 匹配
}
fn plus_one(x: i32) -> i32 {
x + 1; // 作为语句, 故函数没有返回值, 报类型匹配错误(i32)
}
// 条件语句
let number = 3;
if number < 5 { // == / !=
println!("condition was true");
} else {
println!("condition was false");
}
// 简写
let result = if number == 5 { 5 } else { 6 };
// loop循环
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
// while 循环
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
for element in a {
println!("the value is: {element}");
}
}
所有权
所有权是 Rust 最独特的特性,对 Rust 的其他部分有着深远的影响。它使 Rust 能够在无需垃圾收集器的情况下提供内存安全保障,因此理解所有权的工作原理至关重要。
程序语言管理内存的常规方法:
- 垃圾回收机制, 定期检查并回收不再使用的内存
- 由程序员显式进行分配与回收
- 通过所有权系统管理内存, 该系统提供一组规则, 由编译器检查(Rust)
堆栈
堆栈都是在运行代码时可用内存的一部分, 但是二者的数据结构不同.
- 栈stack, 按顺序存值, 反方向移除值(Last In, First Out)
- 栈中所存储的数据的大小是已知且固定的
- 堆heap
- 堆中存储的数据是不确定大小或大小可能会发生改变的
- 堆数据时是通过存储在栈中的一个pointer引用指针来访问的
Stack vs heap 区别
-
储存数据时, 栈相比于堆要更快, 因为栈存储数据的位置始终位于栈顶, 而堆需要额外去寻找空间足够大的位置来存储数据
-
访问数据时, 由于堆访问数据需要通过pointer指针, 所以也慢于栈
所有权规则
-
Rust中每个值都有其所有者
-
一个值仅能有一个所有者
-
当所有者的作用域结束时, 值将被丢弃
按值复制 vs 按引用复制
-
(stack data)按值复制 与 JavaScript 相同;
-
(heap data)按引用复制 等价于 JavaScript 浅拷贝 ** + *限制: *一个值有且只有一个有效引用 *, *简称为"移动"
- 移动意味着所有权被转移到新所有者上, 而原本旧的所有者将失效, 不再可用;
// stack 数据按值复制
let x = 5;
let y = x;
// heap 数据按引用复制
let s1 = String::from("hello");
let s2 = s1; // 引用移动到 s2 后, s1变为无效引用
借用
借用, &符号表示引用, 它允许你引用某个值而无需获取其所有权, 简称 "只用不取"
- 由于没有获取其所有权, 故称为借用
- & 默认是不可变借用, 可变借用需要使用 &mut 符号
- 可变借用有一个巨大的限制, 当一个值已有一个可变借用的引用后, 该值就不能再有其他的引用 (包括可变借用和不可变借用)
- 不可同时存在, 但可以先后存在
- References and Borrowing
- 可变借用有一个巨大的限制, 当一个值已有一个可变借用的引用后, 该值就不能再有其他的引用 (包括可变借用和不可变借用)
fn main() {
let s = String::from("hello");
takes_ownership(s); // s值移动到了 takes_ownership 函数中, 所以 s 此时已不再可用
}
// 引用
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // &s1 并没有改变 s1 的所有权, 因此 s1 依然有效
println!("The length of '{s1}' is {len}.");
change(&mut s); // 可变借用
}
// 错误用法: 不可同时存在
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 报错
let r3 = &s; // 报错
println!("{}, {}, {}", r1, r2, r3);
// 正确用法: 先后存在
// eg1
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
// eg2
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// variables r1 and r2 will not be used after this point
let r3 = &mut s; // no problem
println!("{r3}");
Struct 结构体
类似于js或面向对象语言中的"对象"概念, 包含多个数据字段的自定义数据类型.
// 字面量形式定义
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
// 元组结构体定义
struct Color(i32, i32, i32);
// 结构体方法定义在结构体上下中且它们的第一个参数总是 self, self指向结构体实例对象
#[derive(Debug)] // 通过继承协议(traits)为结构体添加新功能
struct Rectangle {
width: u32,
height: u32,
}
// imple 区块可以同时有多个
impl Rectangle {
// 实例方法, 类似js中class类的实例对象的方法
fn area(&self) -> u32 {
self.width * self.height
}
// 关联方法, 类似js中class类的静态方法,没有self参数不能访问到实例对象,通过结构体名访问和调用
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle::square(10);
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
枚举和模式匹配
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// 四种定义枚举的方式
enum Message {
Quit, // 没有关联任何数据
Move { x: i32, y: i32 }, // 关联具名的字段, 类似struct
Write(String), // tuple struct
ChangeColor(i32, i32, i32), // tuple struct
}
// rust就没有类似其他语言的专用的null值, 但其通过在标准库中预定义的Option来表达当前值是不可用的
enum Option<T> {
None,
Some(T),
}
// 防御性编码, 针对可能存在空值的变量, 通过Option能强制在使用该变量时需要进行判空处理,
// 否则编译器报错
enum Coin {
Penny,
Nickel,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
}
}
// 精确匹配
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
} else {
// other things
}
第三章 管理项目依赖
Package
- 通过组合构建若干个crates来提供一系列的功能函数
- 通过 cargo.toml 来描述如何构建crates
- 一个package可包含多个二进制crates, 但最多只能包含一个库crate
Crate
crate 是 Rust 编译器一次考虑的最小代码量
-
Binary crate: 含main入口函数, 可被编译为可执行文件的源码文件
- 默认crate root 是 src/main.rs
-
Library crate: 不含main入口函数, 不可编译为可执行文件; 主要用于定义项目中共用的各种工具函数
- 默认crate root 是 src/lib.rs
Module
编译时Module,path,use,pub是如何工作的, 以及常见的代码组织方式
-
从crate root根文件开始, 通常是
src/main.rs or src/lib.rs -
定义modules模块, 在根文件中声明模块, 如: mod garden; 编译器将从以下位置寻找模块代码
- 内联, 即直接在根文件中获取, 此时模块的定义方式为:
mod garden {} - 文件 src/garden.rs 或 src/garden/mod.rs
- 内联, 即直接在根文件中获取, 此时模块的定义方式为:
-
子模块的定义以及代码的查询寻址同上一点, 模块位置是相对于所定义其的文件而言
-
引用模块中的某些代码的路径, 假如一个crate存在一个module, 在该crate中其他任何位置都可以引用此module中的代码, 只要在隐私规则运行的情况下, 使用:
crate::garden::vegetables::Asparagus应用即可 -
默认情况下, 模块中的代码对于其父模块而言是私有的; 可通过
pub将其声明为公共 -
为简写长路径, 使用
use, 如use crate::garden::vegetables::Asparagus, 后续直接使用Asparagus即可引用模块中的代码
第四章 Collections
Vectors
存储同类型的多个值
let v: Vec<i32> = Vec::new(); // 显式类型注解, 一般rust会自动推断出类型
let v = vec![1,2,3]; // vec! 快速创建并初始化值
let package_out: Vec<u8> = vec![0; 1024]; // 指定初始化的值以及长度
v.push(1);
let third: &i32 = &v[2]; // 索引溢出会程序panic崩溃
let third: Option<&i32> = v.get(2); // 注意返回的 Option<&T>, 索引溢出返回None, 程序不会panic
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
// 遍历
let v = vec![100, 32, 57];
for i in &v { // 仅读
println!("{i}");
}
for i in &mut v { // 读写
*i += 50;
}
Strings
Strings是特殊的字节集合, 因此很多Vector集合能执行的操作String也能执行.
Strings采用 UTF-8 编码
Strings不支持使用索引值取其中的单个字符, 因为Strings是以Vec字节集合存储的, 而索引只能表示单个字节, 但单个字节并不能总是代表对应的单个字符(某些字符可能是由多个字节来表示的).
let mut s = String::new();
// 创建并初始化
let s = "initial contents".to_string();
let s2 = String::from("another initial way");
// updating
s.push_str("append some tail str"); // push_str追加字符串切片
s.push('l'); // push追加单个字符
// 字符串连接
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 失效, s2 依旧可用
// 组合多个字符串
let result = format!("{s1}-{s2}-{s3}");
切片
let hello = "Здравствуйте";
let s = &hello[0..4]; // Зд
// &hello[0..1] -> error: 这里的一个字符占了2bytes
// 对字符串片段进行作的最佳方法是明确说明 您需要字符或字节
for c in "Зд".chars() {
println!("{c}"); // 3 д
}
for b in "Зд".bytes() {
println!("{b}"); // 208 151 208 180
}
Hash Maps
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 访问
let score = scores.get(&team_name).copied().unwrap_or(0);
// .get() return Option<&T>
// 使用 copied() 获取 Option<i32> 而非 Option<&i32>
// unwrap_or(0) 若不存在key条目, 则返回 0 作为score的值
// 迭代
for (key, value) in &scores {
println!("{key}: {value}");
}
// updating
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25); // overwrite 10
// 当且仅当key不存在时才插入key和value, 否则保留已有的key和value
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{scores:?}"); // {"Yellow": 50, "Blue": 10}
// 基于旧的值来更新
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0); // or_insert 返回可变引用 &mut V
*count += 1; // 解引用获取当前值并+1, 然后通过count可变引用来修改值
}
println!("{map:?}");
第五章 ErrorHandling
两种产生panic的方式:
- 执行了某个错误的操作
- 显示调用
panic!
捕获错误
- match
- unwrap
- expect
- unwrap_or_else
// 1.match 处理错误
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
// 2.closure
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
// 3.unwrap 等价于 match 表达式的缩写版
let greeting_file = File::open("hello.txt").unwrap();
// Result 是 ok 则返回 ok 中的值, 反之, unwrap 会调用 panic!
// 4.expect 等价于 unwrap + 自定义error信息
let g_file = File::open("hello.txt").expect("hello.txt should be included in this project");
传递错误
- 手动 return error
- 使用简写的
?return
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e), // 直接提前 return 该函数
};
println!("username_file: {:?}", username_file);
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
// Result?/Option? : ok继续往下执行; fail则返回错误且结束该函数的执行, 等价于 return error
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
第六章 Trait
Trait类似其他语言中的interface接口的概念, 多用于给其他结构体或类型定义接口
pub trait Summary {
// 默认实现
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
// 实现 Summary 接口
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
// trait 作为参数使用, 指定多个 => notify(item: &(impl Summary + Display))
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// trait 作为返回值
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from("you probably already know, people"),
reply: false,
repost: false,
}
}
第七章 函数
闭包
闭包是一个匿名函数, 其可保存变量或作为参数传递给其他函数. 闭包函数可以从定义其的作用域中捕获变量.
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked()) // 闭包函数主体调用了 most_stocked
}
// Options<T>.unwrap_or_else以一个闭包作为入参, 若闭包有参数, 会位于双竖线之间, 即|args|
// user_preference => None 则执行闭包函数
// user_preference => Some 则直接返回 Some 值
// 其他定义方式:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
第八章 Cargo & crates.io
使用release profiles自定义构建
两类profiles
- 运行
cargo build时使用 dev profiles - 运行
cargo build --release时使用 release profiles
// cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
发布crate到crates.io
-
创建 crates account
-
cargo login
-
cargo publish
- 在 cargo.toml 上添加必要的一些信息字段, 如: name, version, license 等
-
cargo yank --vers 1.0.0 [--undo]
- 弃用某个版本, 防止被其他人依赖使用; --undo 撤销弃用
第九章 Pointer
Box
Box允许你将数据存储到heap堆, 而只在stack栈中保留一个指向该堆的指针.
和JavaScript中的基本数据类型自动拆/装箱类似, 如: 99 -> Number(99)
let num = Box::new(6);
应用场景之一: 在定义递归嵌套的数据类型时, 由于Rust无法确定需要为其分配多大的空间, 所以递归数据类型无法直接定义, 而是需要通过 Box 来间接完成定义.
因为 Box实际上是一个指针, 且这个指针占用的空间大小是固定的, 不会因为所指向的实际数据的大小而改变.
当一个 Box 超出作用域时, 指针以及指向的数据均会被回收.
自定义 MyBox
实现一个类似于 Box 的类型, 看看如何使用 Deref .
fn main() {
let x = 5;
let y = &x;
let z = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // *y 解引用之后就能访问到引用 y 所指向的值
assert_eq!(5, *z); // 和普通引用一样的使用方式
}
普通引用和Box都是引用, 即一种指针, 使用方式是一样的
实现一个自定义的智能指针:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
第十章 并发/线程
Concurrency 并发, 高速交替执行
parallel 并行, 多核同时执行
使用Thread
将计算量拆分为多个线程同时执行多个任务, 能显著提升性能, 但会增加程序的复杂度.
多线程的核心难点是在于多个线程是同时执行的, 所以无法保证多个线程中的代码的执行顺序, 这就会导致一些问题:
- 竞争条件,即线程以不一致的顺序访问数据或资源
- 死锁,即两个线程互相等待,导致两个线程无法继续执行
- 仅在某些情况下发生且难以重现和可靠修复的错误
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1)); // 线程短暂停止执行,从而允许另一个线程运行
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap(); // 阻塞当前的主线程, 直到handle句柄代表的子线程终止
}
默认情况下, 一旦主线程执行完毕, 无论其他子线程是否执行完都会关闭;
若需等待子线程执行完成再结束主线程则需要调用 thread.spawn().JoinHandle.join() 方法来阻塞主线程,
阻塞 线程意味着该线程无法执行工作或退出。
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1)); // 线程短暂停止执行,从而允许另一个线程运行
}
});
// 阻塞当前的主线程, 直到handle句柄代表的子线程终止;
// 执行完 handle 线程之后, 才会指向 main 中的for loop
handle.join().unwrap();
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
线程中使用 move closures
我们经常在传递给 thread::spawn 的闭包中使用 move 关键字, 因为闭包将从其上下文环境中获得它所使用的值的所有权,从而将这些值的所有权从一个线程转移到另一个。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// Rust 推断出如何捕获 v ,并且因为 println! 只需要对 v 的引用,所以闭包会尝试借用 v 。
// 然而,有一个问题:Rust 无法判断生成的线程将运行多长时间,因此它不知道对 v 引用是否始终有效。
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
// 通过在闭包前添加 move 关键字,我们强制闭包获取它正在使用的值的所有权,
// 而不是让 Rust 推断它应该借用这些值。
let handle2 = thread::spawn(move || {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
线程之间传输数据
-
channel通道
使用消息传递 Message passing 在线程之间传输数据.
一种日益流行的确保安全并发的方法是消息传递 ,即线程通过相互发送包含数据的消息进行通信。Go语言文档中有一句口号表达了这种理念:“不要通过共享内存进行通信;相反,要通过通信来共享内存。 ”
通道是一个通用的编程概念,通过它可以将数据从一个线程发送到另一个线程.
通道由两部分组成:发送器和接收器。发送器部分位于上游,将小黄鸭放入河中;接收器部分位于下游,将小黄鸭放入河中。代码的一部分使用要发送的数据调用发送器上的方法,另一部分检查接收端是否有消息到达。如果发送器或接收器部分断开连接,则通道关闭 。
因此, rust标准库中提供了一种消息传递的实现: channel 通道.
Mpsc 代表 "多生产者, 单消费者" 的含义.
rx.recv阻塞主线程执行并直到从channel中接收到一个值为止; 当 tx 发送端关闭时,rx.recv会收到一个错误信号从而得知不会再有消息发来, 再终止阻塞.rx.try_recv不阻塞主线程执行, 执行后立即返回Result<T, E>
注意: 这两个方法执行后再接收到一个值就执行完毕, 不会一直等待并持续接收值; 若需要持续获取值, 可使用for...in 迭代 tx
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
// 同一channel多生产者, 直接clone即可 let tx1 = tx.clone();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
// tx 作为迭代器读取多个消息
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi from"),
String::from("the thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
// channel 关闭, rx 作为迭代器也会结束
for received in rx {
println!("Got: {received}");
}
}
2. ### 共享状态/mutex
共享状态也是一种实现线程间通信的方法.
共享内存并发就像多重所有权:多个线程可以同时访问同一内存位置。
多重所有权可能会增加复杂性,因为需要管理这些不同的所有者。Rust 的类型系统和所有权规则极大地帮助了这种管理的正确性, 如: 互斥体(mutex)是并发共享内存中常见的概念之一.
mutex 是 mutual exclusion 的缩写, 互斥锁只允许一个线程在任意时刻访问某些数据.
互斥锁使用难度高, 需要严格遵循下面的规则:
- 您必须在使用数据之前尝试获取锁
- 当您处理完互斥锁保护的数据后,您必须解锁数据,以便其他线程可以获取锁
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
// 问题: 多个线程都需要取得 counter 的所有权, 即需要用到多重所有权
for _ in 0..10 {
// 创建一个新的Arc实例, 类似创建了一个新指针引用了同一数据源
let counter = Acr::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());
}
Arc是一种在并发情况下可以安全使用的 原子引用计数 类型.
Arc 是用于多线程间共享所有权的智能指针。它的核心作用是通过线程安全的引用计数机制,让多个线程同时持有对同一数据的不可变引用(或配合 Mutex 实现可变访问),同时保证内存安全。
原子类型的工作方式类似于原始类型,但可以安全地跨线程共享。
为什么不是所有 原始类型 都具备原子性,为什么标准库类型没有默认实现
Arc<T>呢?原因是线程安全会带来性能损失,而这种损失只有在真正需要时才需要承担。如果你只是在单线程中对值执行操作,那么如果代码不需要强制执行原子性提供的保证,运行速度会更快。
第十一章 异步编程
在同一时间点执行多个操作, 通常有两种技术: 并发和并行
futures & async/await
futures类似于JavaScript中的promise概念, 在一个async的区块或者函数中可以使用await关键词去等待一个futures.