记 Rust 学习

26 阅读21分钟

Rust 是一门高性能、 内存安全、并发友好的现代系统编程语言, 其设计目标是在不牺牲性能的前提下解决内存安全和并发问题,同时保持开发效率.

第一章

1. Hello world

rustc 编译 *.rs 文件生成*.exe可执行文件. image.png

2.Cargo

cargorust的包管理器和构建工具, 跟随rust自动安装, 二者关系类似于nodejsnpm.

image.png

使用 cargo 创建项目

cargo new yourProjectName
cd ./yourProjectName

# 检查文件是否存在编译错误, 不生成可执行文件
cargo check

# 编译
cargo build

# 运行
cargo run

基本目录结构如下:

image.png

Cargo.toml类似于package.json管理项目配置以及依赖.

第二章 常见概念

变量和可变性

默认情况下, rust 变量是不可变的 (immutable), 即一旦一个值被绑定到一个变量后, 不能再改变该变量的值.

let vs const 的区别:

  • Const 与 mut 不能共用, 而 let 可以
  • Const 常量绑定的值始终都不能修改且必须进行类型注释
  • Const 常量的初始值只能设置为常量表达式的值, 而不能设置为在运行时计算出来的结果值
    • eg: const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

变量遮蔽 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 能够在无需垃圾收集器的情况下提供内存安全保障,因此理解所有权的工作原理至关重要。

程序语言管理内存的常规方法:

  1. 垃圾回收机制, 定期检查并回收不再使用的内存
  2. 由程序员显式进行分配与回收
  3. 通过所有权系统管理内存, 该系统提供一组规则, 由编译器检查(Rust)

堆栈

堆栈都是在运行代码时可用内存的一部分, 但是二者的数据结构不同.

  • 栈stack, 按顺序存值, 反方向移除值(Last In, First Out)
    • 栈中所存储的数据的大小是已知且固定的
  • 堆heap
    • 堆中存储的数据是不确定大小或大小可能会发生改变的
    • 堆数据时是通过存储在栈中的一个pointer引用指针来访问的

Stack vs heap 区别

  • 储存数据时, 栈相比于堆要更快, 因为栈存储数据的位置始终位于栈顶, 而堆需要额外去寻找空间足够大的位置来存储数据

  • 访问数据时, 由于堆访问数据需要通过pointer指针, 所以也慢于栈

所有权规则

  1. Rust中每个值都有其所有者

  2. 一个值仅能有一个所有者

  3. 当所有者的作用域结束时, 值将被丢弃

按值复制 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 符号
    • 可变借用有一个巨大的限制, 当一个值已有一个可变借用的引用后, 该值就不能再有其他的引用 (包括可变借用和不可变借用)
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是如何工作的, 以及常见的代码组织方式

  1. 从crate root根文件开始, 通常是 src/main.rs or src/lib.rs

  2. 定义modules模块, 在根文件中声明模块, 如: mod garden; 编译器将从以下位置寻找模块代码

    1. 内联, 即直接在根文件中获取, 此时模块的定义方式为: mod garden {}
    2. 文件 src/garden.rs 或 src/garden/mod.rs
  3. 子模块的定义以及代码的查询寻址同上一点, 模块位置是相对于所定义其的文件而言

  4. 引用模块中的某些代码的路径, 假如一个crate存在一个module, 在该crate中其他任何位置都可以引用此module中的代码, 只要在隐私规则运行的情况下, 使用: crate::garden::vegetables::Asparagus应用即可

  5. 默认情况下, 模块中的代码对于其父模块而言是私有的; 可通过 pub 将其声明为公共

  6. 为简写长路径, 使用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的方式:

  1. 执行了某个错误的操作
  2. 显示调用 panic!

捕获错误

  1. match
  2. unwrap
  3. expect
  4. 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");

传递错误

  1. 手动 return error
  2. 使用简写的? 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

  1. 运行 cargo build 时使用 dev profiles
  2. 运行 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

将计算量拆分为多个线程同时执行多个任务, 能显著提升性能, 但会增加程序的复杂度.

多线程的核心难点是在于多个线程是同时执行的, 所以无法保证多个线程中的代码的执行顺序, 这就会导致一些问题:

  1. 竞争条件,即线程以不一致的顺序访问数据或资源
  2. 死锁,即两个线程互相等待,导致两个线程无法继续执行
  3. 仅在某些情况下发生且难以重现和可靠修复的错误
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();
}

线程之间传输数据

  1. 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 的缩写, 互斥锁只允许一个线程在任意时刻访问某些数据.

互斥锁使用难度高, 需要严格遵循下面的规则:

  1. 您必须在使用数据之前尝试获取锁
  2. 当您处理完互斥锁保护的数据后,您必须解锁数据,以便其他线程可以获取锁
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.

相关文档