Skip to content

Day 23: 智能指针:Arc与Mutex

昨天我们学习了Rc和RefCell,它们解决了单线程环境下的共享所有权和内部可变性。但Rc是非线程安全的——它在多线程场景下可能导致数据竞争和内存不安全。

Rust的类型系统将线程安全也纳入了类型检查。今天我们将学习两个用于多线程的智能指针:Arc(Atomic Reference Counting)和Mutex(Mutual Exclusion)。

Arc是Rc的线程安全版本。“原子”意味着引用计数的增减操作是原子的(不可中断的),能够在多线程环境下正确工作。

use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(5);
// 使用Arc::clone()克隆
let data2 = Arc::clone(&data);
// 在新线程中使用
let handle = thread::spawn(move || {
println!("线程中的数据: {}", data2);
});
// 主线程也可以访问
println!("主线程中的数据: {}", data);
handle.join().unwrap();
}

Arc::clone()同样不会深拷贝数据,只是增加引用计数。Arc的额外开销来自于原子操作的实现,因此如果没有多线程共享需求,应该使用Rc。

use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
// 创建多个线程,每个线程都引用同一个Arc
for i in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("线程 {} 看到的数据: {:?}", i, data_clone);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("最终数据: {:?}", data);
}

每个线程都获得了数据的独立引用,Arc保证所有线程看到一致的引用计数和数据。

Arc提供了共享所有权,但如果多个线程同时修改共享数据,就会产生数据竞争。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..5 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
println!("计数: {}", *num);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("最终计数: {}", *counter.lock().unwrap());
}

lock()方法返回MutexGuard,这是一个 RAII 风格的锁。当MutexGuard离开作用域时,锁自动释放,即使发生panic也不会死锁。

5. Arc 和 Mutex 组合:多线程共享可变状态

Section titled “5. Arc 和 Mutex 组合:多线程共享可变状态”

这是Rust多线程编程中最常用的模式之一:

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
struct BankAccount {
balance: f64,
}
fn main() {
let account = Arc::new(Mutex::new(BankAccount { balance: 1000.0 }));
let mut handles = vec![];
// 存款线程
let deposit_handle = {
let account = Arc::clone(&account);
thread::spawn(move || {
let mut bal = account.lock().unwrap();
*bal.balance += 500.0;
println!("存款后余额: {}", bal.balance);
})
};
handles.push(deposit_handle);
// 取款线程
let withdraw_handle = {
let account = Arc::clone(&account);
thread::spawn(move || {
thread::sleep(Duration::from_millis(10)); // 确保后执行
let mut bal = account.lock().unwrap();
*bal.balance -= 200.0;
println!("取款后余额: {}", bal.balance);
})
};
handles.push(withdraw_handle);
for handle in handles {
handle.join().unwrap();
}
println!("最终余额: {}", *account.lock().unwrap().balance);
}

死锁避免:Mutex不会自动检测死锁,开发者需要自行避免。常见的死锁原因是一个线程持有锁的同时等待另一个线程释放锁。

use std::sync::{Arc, Mutex};
use std::thread;
// 错误示例:可能导致死锁
fn deadlock_example() {
let mutex1 = Arc::new(Mutex::new(1));
let mutex2 = Arc::new(Mutex::new(2));
let mutex1_clone = Arc::clone(&mutex1);
let mutex2_clone = Arc::clone(&mutex2);
let handle1 = thread::spawn(move || {
let _lock1 = mutex1_clone.lock().unwrap();
println!("线程1持有mutex1");
let _lock2 = mutex2_clone.lock().unwrap(); // 可能永久等待
});
let handle2 = thread::spawn(move || {
let _lock2 = mutex2_clone.lock().unwrap();
println!("线程2持有mutex2");
let _lock1 = mutex1_clone.lock().unwrap(); // 可能永久等待
});
// 使用try_lock()替代lock()可以避免永久阻塞
}

Poisoning:如果持有锁的线程panic了,Mutex会被”毒害”(poisoned)。后续lock()会返回PoisonError,防止数据处于不一致状态。

use std::sync::Mutex;
fn main() {
let mutex = Mutex::new(5);
// 手动模拟panic场景
let result = std::panic::catch_unwind(|| {
let _guard = mutex.lock().unwrap();
panic!("假装panic");
});
match result {
Ok(_) => println!("没有panic"),
Err(_) => println!("Mutex被毒害了"),
}
}

对于读多写少的场景,Mutex的效率不高。RwLock允许多个读线程或单个写线程并发访问。

use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(0));
let mut handles = vec![];
// 读线程
for _ in 0..3 {
let data_clone = Arc::clone(&data);
handles.push(thread::spawn(move || {
let value = data_clone.read().unwrap();
println!("读取值: {}", *value);
}));
}
// 写线程
let write_clone = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut value = write_clone.write().unwrap();
*value += 1;
println!("写入新值: {}", *value);
}));
for handle in handles {
handle.join().unwrap();
}
}

今天我们学习了Rust中用于多线程编程的两种关键智能指针:

  1. Arc:原子引用计数指针,用于多线程间共享所有权。它是Rc的线程安全版本。

  2. Mutex:互斥锁,通过独占访问保证线程安全的数据修改。配合Arc使用,是Rust多线程安全编程的基础。

  3. RwLock:读写锁,提供了比Mutex更好的读并发性能。

Rust的所有权系统与线程安全紧密结合,编译器会在编译期就阻止大部分数据竞争问题,这使得Rust成为编写安全并发代码的理想选择。