学习rust_day12

引用循环可能导致内存泄漏

内存泄漏

永远不会被清理掉的内存

  • Rust的安全保障使得意外的内存泄漏很难发生,但不是不可能
  • 完全防止内存泄漏并不是Rust的保证之一 一> 内存泄漏是内存安全的
  • 例如:通过Rc和RefCell就可创建出循环引用,导致内存泄漏
    • 各个项的引用数永不为0

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
use std::{cell::RefCell, rc::Rc};
use crate::List::{Cons, Nil};

fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

println!("1 {}", Rc::strong_count(&a));
println!("2 {:?}", a.tail());

let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

println!("3 {}", Rc::strong_count(&a));
println!("4 {}", Rc::strong_count(&b));
println!("5 {:?}", b.tail());

if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}

println!("6 {}", Rc::strong_count(&b));
println!("7 {}", Rc::strong_count(&a));

// println!("8 {:?}", a.tail()); 无限循环输出报错
}

#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil
}

impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None
}
}
}

防止引用循环

防止引用循环:将Rc转换为Weak

  • 通过Rc:clone创建强引用,增加strong_count。只有当strong_.count为0时,Rc指向的值才会被清理
  • 通过Rc:downgrade创建弱引用,增加weak_count。弱引用不表示所有权,不影响清理时机,因此不会导致循环引用
  • Weak不能直接使用它指向的值,需要通过upgrade方法检查该值是否仍然存在:
    • 如果Rc仍存在,upgrade返回Some(Rc),否则返回None
  • 应用场景:用树形结构(包含父子关系)替代单向链表

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
value: i32,
childern: RefCell<Vec<Rc<Node>>>,
parent: RefCell<Weak<Node>>
}

fn main() {
let leaf = Rc::new(Node {
value: 3,
childern: RefCell::new(vec![]),
parent: RefCell::new(Weak::new())
});

println!("{:?}", leaf.parent.borrow().upgrade());

let branch = Rc::new(Node {
value: 5,
childern: RefCell::new(vec![Rc::clone(&leaf)]),
parent: RefCell::new(Weak::new())
});

*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

println!("{:?}", leaf.parent.borrow().upgrade());
}

多线程 – 无畏并发

  • 目标:安全高效的并发编程
  • 独特方法:利用所有权和类型系统在编译时防止并发错误
  • 优势:在开发阶段而非生产环境中发现错误
  • 灵活性:为不同并发模型提供多种工具

使用多线程同时运行代码

  • 在大多数当前的操作系统中,被执行程序的代码是在一个进程中运行的,操作系统会同时管理多个进程(Process)。
  • 在一个程序内部,你也可以有独立的部分同时运行。运行这些独立部分的功能被称为线程(Thread)。
  • 例如,一个网络服务器可以有多个线程,这样它就可以同时响应多个请求。

多线程可导致的问题

  • 竞态条件(race condition)
    • 即线程以不一致的顺序访问数据或资源
  • 死锁(Deadlocks)
    • 即两个线程相互等待,导致两个线程都无法继续
  • 只在某些情况下发生的错误,难以可靠地重现和修复

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::thread;
use std::time::Duration;

fn main() {
//当主线程结束时子线程也会结束经过其尚未完成任务,通过使用JoinHandle的join函数来解决
let handle = thread::spawn( || {
for i in 1..10 {
println!("hi number {i} form th 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(); //会阻塞当前线程
}

在线程中使用move闭包

1
2
3
4
5
6
7
8
9
10
11
use std::thread;

fn main() {
let v = vec![1, 2, 3];

let handle = thread::spawn(move || { //因为Rust不能确定v比该线程活的更长
println!("{v:?}");
});

handle.join().unwrap();
}

消息传递
message passing

  • 线程或actors通过发送包含数据的消息来相互通信
  • Go语言口号:”不要通过共享内存来通信;而是通过通信来共享内存。”
  • Rust的标准库提供了通道(channel)的实现

通道
channel

  • 通道是一种程序设计概念,用于在不同线程之间发送数据
  • 两个核心部分:发送端(transmitter)和接收端(receiver)
  • 当通道的任一端(发送端或接收端)被丢弃时,我们说通道被关闭了

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::sync::mpsc;
use std::thread;

fn main() {
//mpsc意味着可以有多个发送者,但只能有一个接收者
let (tx, rx) = mpsc::channel();
//建立更多发送端可以使用 let tx1 = tx.clone();

thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
//此后已经不能使用val,防止主线程修改val
});

//可选recv和try_recv,前者会阻塞当前线程直到收到一个值,后者不会阻塞而是立刻返回一个Result
let received = rx.recv().unwrap();
println!("Got: {received}");
}

共享状态并发

共享数据

  • 另一种方法是让多个线程访问相同的共享数据。
  • 在某种程度上
    • 任何编程语言中的通道都类似于单一所有权
    • 共享内存并发就像多重所有权:多个线程可以同时访问相同的内存位置

Mutex互斥锁

  • Mutex:Mutual Exclusion
  • 互斥锁在任何给定时间只允许一个线程访问某些数据
  • 要访问互斥锁中的数据,线程必须请求获取互斥锁的锁
  • 锁是互斥锁的一种数据结构,用于跟踪谁当前拥有对数据的独占访问权。
  • 互斥锁被描述为通过锁定系统来保护它所持有的数据。

Mutex两条规则

  • 在使用数据之前,你必须尝试获取锁
  • 当你使用完互斥锁保护的数据后,必须解锁数据,以便其他线程可以获取锁

例:

1
2
3
4
5
6
7
8
9
10
11
12
use std::sync::Mutex;

fn main() {
let m = Mutex::new(5);

{
let mut num = m.lock().unwrap(); //lock时会阻塞当前线程
*num = 6;
} //走出作用域自动解锁

println!("m = {m:?}");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::sync::Mutex;
use std::thread;
use std::sync::Arc;

//多线程下的多重所有权
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());
}

Sync & Send traits

  • Rust语言本身的并发特性非常少
  • 大多数并发功能都是标准库的一部分,而不是语言本身
  • 可以编写自己的并发功能或使用第三方库
  • 个并发概念:std::marker traits – Sync和Send

Send trait

  • Send(marker trait): 所有权可以在线程之间转移
  • 几乎所有Rust类型都是Send
  • 但有一些例外,例如Rc不是Send
  • Rc仅用于单线程情况
  • Rust的类型系统和trait约束确保不会意外地将非Send类型跨线程发送
  • 完全由Send类型组成的任何类型也自动标记为Send
  • 几乎所有原始类型都是Send原始指针除外

Sync trait

  • Sync(marker trait):可以安全地从多个线程|用实现该trait的类型
  • 如果&T是Send,则类型T是Sync
    • 即该引用可以安全地发送到另一个线程
  • 原始类型是Sync
  • 完全由Sync类型组成的类型也是Sync

线程安全性与Sync

  • Sync是Rust中最接近”线程安全”的概念

    • “线程安全”指特定数据可以被多个并发线程安全使用
  • 分开Send和Sync特性的原因:一个类型可能是其中之一,两者都是,或两者都不是:

    • Rc:既不是Send也不是Sync
    • RefCell<-T>:是Send(如果T是Send),但不是Sync
    • Mutex:是Send也是Sync,可用于多线程共享访问
    • MutexGuard<‘a,T>:是Sync(如果T是Sync)但不是Send

学习rust_day12
https://zlsf-zl.github.io/2026/03/21/学习rust-day12/
作者
ZLSF
发布于
2026年3月21日
许可协议