跨线程局部可变性 ------ Mutex / RwLock / Atomic
一、从单线程到多线程
1.1 Cell 和 RefCell 的线程限制
Cell 和 RefCell 只能在单线程中使用------它们不是 Sync:
rust
use std::cell::Cell;
use std::thread;
let c = Cell::new(42);
// thread::spawn(move || { // ❌ Cell<i32> 不是 Send
// c.set(100);
// });
// 即使通过 Arc 包装:
use std::sync::Arc;
// let shared = Arc::new(Cell::new(42));
// 这也不行------Cell 不是 Sync,Arc 不能跨线程共享
这是因为 Cell 和 RefCell 内部没有使用原子操作,多线程同时访问会导致数据竞争。
1.2 跨线程可变性的需求
多线程场景中常见的模式:
rust
// 线程安全的计数器
// 线程安全的缓存
// 共享的配置更新
// 跨线程的工作队列
这些都需要:多个线程可以访问同一数据,且数据可以被修改。
1.3 C 中的方式
c
#include <pthread.h>
// C 中:锁和数据是分离的概念
struct SharedData {
pthread_mutex_t mutex;
int counter;
};
void increment(struct SharedData* data) {
pthread_mutex_lock(&data->mutex);
data->counter++;
pthread_mutex_unlock(&data->mutex);
}
C 的问题:
-
锁和数据是分离的------你可以修改数据而忘记加锁
-
锁的类型安全靠程序员------可以锁 mutex A 而操作 mutex B 保护的数据
-
忘记 unlock → 死锁
1.4 Rust 的方案
Rust 中,Mutex<T> 将锁和数据绑定在一个类型中:
arduino
C: pthread_mutex_t lock; int counter; ← 分离
Rust: Mutex<i32> ← 锁 + 数据绑定在一起
只能通过锁来访问数据,不能绕过。
二、Mutex<T> ------ 互斥锁保护的数据
2.1 基本用法
rust
use std::sync::Mutex;
use std::thread;
// 创建一个互斥锁保护的数据
let counter = Mutex::new(0);
// 加锁并访问数据
{
let mut data = counter.lock().unwrap();
*data += 1;
// data 离开作用域时自动解锁
}
println!("{}", *counter.lock().unwrap()); // 1
关键点:
-
Mutex::lock()返回MutexGuard<T>------一个智能指针,实现了DerefMut<Target=T> -
MutexGuard在Drop时自动解锁------不可能忘记 unlock -
lock()返回Result<MutexGuard<T>, PoisonError>------如果其他线程在持有锁时 panic,Mutex被标记为"毒化"(poisoned)
2.2 多线程共享 Mutex
Mutex 本身是 Send 和 Sync 的,所以可以通过 Arc 在多线程间共享:
rust
use std::sync::{Mutex, Arc};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut data = c.lock().unwrap();
*data += 1;
}); // 线程结束时 MutexGuard 被 drop → 自动解锁
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
println!("结果: {}", *counter.lock().unwrap()); // 10
2.3 MutexGuard 的自动解锁
rust
fn example() {
let lock = Mutex::new(vec![1, 2, 3]);
{
let mut guard = lock.lock().unwrap();
guard.push(4);
// guard 在这里被 drop → unlock
}
// 另一个线程可以获取锁了
let mut guard = lock.lock().unwrap();
guard.push(5);
// 函数结束时 guard 被 drop → unlock
}
这是 Rust 对比 C 的核心优势:C 的 pthread_mutex_unlock 要靠人记,Rust 的 MutexGuard::drop 靠编译器插装。 不存在"忘记 unlock"------因为 Drop 保证执行。
2.4 中毒(Poison)机制
如果持有锁的线程 panic 了,Mutex 被标记为 poisoned:
rust
use std::sync::Mutex;
let m = Mutex::new(42);
// 线程 1:持有锁时 panic
let handle = std::thread::spawn(move || {
let _guard = m.lock().unwrap();
panic!("出错了"); // Mutex 被毒化
});
handle.join().is_err(); // true
// 线程 2:尝试获取锁
// match m.lock() {
// Ok(mut val) => *val += 1,
// Err(poisoned) => {
// // 可以强制获取锁
// let mut val = poisoned.into_inner();
// *val += 1;
// }
// }
中毒机制防止了"一个线程崩溃后其他线程读取损坏数据"的场景。如果你确信数据没问题,可以用 into_inner() 或 lock().unwrap() 强制获取。
2.5 C vs Rust 对比
| 维度 | C (pthread_mutex_t) |
Rust (Mutex<T>) |
|---|---|---|
| 锁与数据的关系 | 分离(靠程序员关联) | 绑定(Mutex<T> 内含数据) |
| 加锁 | pthread_mutex_lock(&m) |
m.lock() |
| 解锁 | pthread_mutex_unlock(&m) |
自动(MutexGuard::drop) |
| 忘记解锁 | 常见 bug | 不可能(编译期保障) |
| 锁了错误的数据 | 可能(锁 A,操作 B) | 不可能(只能通过 guard 访问数据) |
| 异常安全 | 异常时可能无法 unlock | panic 时自动 unlock(但标记 poisoned) |
| 死锁 | 可能(忘记 unlock) | 减少(自动 unlock,但仍可能死锁) |
三、RwLock<T> ------ 读写锁
3.1 何时使用读写锁
当数据的读操作远多于写操作时,RwLock 比 Mutex 更高效------它允许多个读者同时访问:
rust
use std::sync::RwLock;
let config = RwLock::new(Config::default());
// 多个线程可以同时读
fn read_config(config: &RwLock<Config>) -> Config {
config.read().unwrap().clone()
}
// 写操作是独占的
fn write_config(config: &RwLock<Config>, new: Config) {
*config.write().unwrap() = new;
}
3.2 基本用法
rust
use std::sync::RwLock;
let data = RwLock::new(vec![1, 2, 3]);
// 读操作------可以多个线程同时
{
let r = data.read().unwrap();
println!("{}", r.len()); // 3
} // r 被 drop,读锁释放
// 写操作------独占
{
let mut w = data.write().unwrap();
w.push(4);
} // w 被 drop,写锁释放
3.3 Mutex vs RwLock 选择
读多写少 → RwLock(多个读者可同时)
读写相当 → Mutex(更简单,开销更低)
写多读少 → Mutex
Mutex 在内部实现上比 RwLock 简单,所以当读写频率差不多时,Mutex 通常更快。只有在读远多于写(如缓存、配置)时才应该使用 RwLock。
四、原子类型 ------ AtomicBool / AtomicU32 / AtomicUsize / AtomicPtr
4.1 何时使用原子类型
当你要保护的只是一个简单的整型或布尔值时,Mutex<i32> 的开销太大了------加锁和解锁涉及内核调用或至少是系统级的同步。此时使用原子类型:
rust
use std::sync::atomic::{AtomicU32, Ordering};
// 原子计数器------比 Mutex<u32> 快得多
let counter = AtomicU32::new(0);
// 线程安全的递增
counter.fetch_add(1, Ordering::SeqCst);
4.2 基本类型
| 原子类型 | 对应的普通类型 |
|---|---|
AtomicBool |
bool |
AtomicU8 / AtomicI8 |
u8 / i8 |
AtomicU16 / AtomicI16 |
u16 / i16 |
AtomicU32 / AtomicI32 |
u32 / i32 |
AtomicU64 / AtomicI64 |
u64 / i64 |
AtomicUsize / AtomicIsize |
usize / isize |
AtomicPtr<T> |
*mut T |
4.3 常用操作
rust
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(std::thread::spawn(move || {
c.fetch_add(1, Ordering::Relaxed); // 原子递增
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.load(Ordering::Relaxed)); // 10
常用方法:
-
load(order):读取值 -
store(val, order):写入值 -
fetch_add(n, order):原子加 -
fetch_sub(n, order):原子减 -
swap(val, order):交换 -
compare_exchange(prev, next, ok_order, fail_order):CAS
4.4 C 中的原子操作 vs Rust
c
// C11 的原子操作
#include <stdatomic.h>
atomic_int counter = ATOMIC_VAR_INIT(0);
atomic_fetch_add(&counter, 1, memory_order_relaxed);
rust
// Rust ------ 对应代码
use std::sync::atomic::{AtomicI32, Ordering};
let counter = AtomicI32::new(0);
counter.fetch_add(1, Ordering::Relaxed);
C 与 Rust 的原子操作在运行时完全相同------都编译为 CPU 的原子指令。区别是:
-
Rust 中原子类型是
Sync的,可以安全地跨线程共享 -
Rust 中
Ordering是枚举,不会出现 C 中memory_order拼写错误 -
Rust 不允许"普通赋值 + 原子操作"混用------
counter = 5对原子类型是编译错误
4.5 什么时候用 Mutex,什么时候用 Atomic
rust
// Atomic ------ 只需对单个整数/布尔做简单操作
let flag = AtomicBool::new(true);
flag.store(false, Ordering::Release);
// Mutex ------ 需要保护复杂数据或需要一致性读-改-写
let config = Mutex::new(HashMap::new());
let mut map = config.lock().unwrap();
map.insert("key", "value");
| 场景 | 用 |
|---|---|
| 计数器 | AtomicU32 / AtomicU64 |
| 标志位(开关、停止信号) | AtomicBool |
| 指针的原子交换 | AtomicPtr<T> |
| 复杂数据的互斥访问 | Mutex<T> / RwLock<T> |
五、对比总表
| 类型 | 所有权 | 可变性 | 线程安全 | 适用场景 |
|--------------|-----|--------------|-------|---------------|---|
| Cell<T> | 单线程 | 内部可变,Copy 类型 | !Sync | 计数、标志 | |
| RefCell<T> | 单线程 | 内部可变,运行时检查 | !Sync | 缓存、Rc+RefCell |
| Mutex<T> | 多线程 | 互斥锁保护 | Sync | 复杂数据的跨线程共享 |
| RwLock<T> | 多线程 | 读共享/写独占 | Sync | 读多写少的场景 |
| 原子类型 | 多线程 | 原子指令 | Sync | 计数器、标志位 |
选型流程
swift
需要可变性?
│
├── 单线程 →
│ ├── 可通过 &mut self 解决?→ &mut T(最佳)
│ ├── 需在 &self 下修改?
│ │ ├── T: Copy → Cell<T>
│ │ └── T: !Copy → RefCell<T>
│ └── 需共享所有权?→ Rc<Cell/RefCell<T>>
│
└── 多线程 →
├── 只需简单整数/布尔操作?→ AtomicU32 / AtomicBool
├── 需保护复杂数据?
│ ├── 读多写少 → RwLock<T>
│ └── 读写相当 → Mutex<T>
└── 需共享所有权?→ Arc<Mutex/RwLock<T>>
六、与 C 程序员的对话
"Mutex 不就是 pthread_mutex_t"
C 程序员 :"
Mutex不就是pthread_mutex_t包了一层吗?有什么新东西?"
Rust :"运行时完全一样,都是操作系统提供的互斥锁。区别在于:C 中锁和数据是分离的,你可以加锁 A 操作 B------编译器不会阻止。Rust 中Mutex<T>把锁和数据绑在一起,你只能 通过lock()返回的 guard 访问数据,不可能绕过。"
c
// C ------ 锁和数据分离
pthread_mutex_t mtx;
int data;
pthread_mutex_lock(&mtx);
data++; // 正确:锁了
pthread_mutex_unlock(&mtx);
data++; // ❌ 忘了加锁------编译通过,运行出错
rust
// Rust ------ 锁和数据绑定
let data = Mutex::new(0);
let mut guard = data.lock().unwrap();
*guard += 1; // ✅ 通过 guard 访问
drop(guard);
// data += 1; // ❌ 编译错误!不能直接访问 Mutex 内部的数据
"原子操作不就是 __sync_fetch_and_add"
C 程序员 :"原子操作在 C 中也有,
__sync_fetch_and_add用了很多年。"
Rust :"运行时确实一样。但 C 的原子操作有三点不如 Rust:一是memory_order参数是整数常量,写错编译器不报错;二是 C 中原子类型和非原子类型可以混用;三是 C 中没有Sync的概念------需要你自己保证跨线程安全。"
c
// C ------ memory_order 是整数,可能拼错
atomic_fetch_add(&counter, 1, memory_order_relaxed); // 对
atomic_fetch_add(&counter, 1, memory_order_relax); // 错!编译器不报
rust
// Rust ------ Ordering 是枚举,编译期检查
counter.fetch_add(1, Ordering::Relaxed); // ✅
// counter.fetch_add(1, Relaxed); // ❌ 编译错误:拼写检查
七、小结
从单线程到多线程的完整可变性工具链
r
单线程内部可变性: Cell<T> --- Copy 类型,零开销
RefCell<T> --- 任意类型,运行时检查
多线程内部可变性: AtomicT --- 简单类型(整数/布尔)
Mutex<T> --- 复杂数据,互斥访问
RwLock<T> --- 读多写少,读共享
C vs Rust 对比
| 维度 | C | Rust |
|---|---|---|
| 锁与数据的关系 | 分离(靠约定) | 绑定(类型保证) |
| 解锁 | 手动 pthread_mutex_unlock |
自动(MutexGuard::drop) |
| 忘记解锁 | 常见 | 不可能 |
| 原子操作安全 | 靠程序员 | 类型系统保证 |
| 跨线程共享 | 靠程序员保证 | Send + Sync 编译器检查 |
一句话总结 :C 中跨线程可变性靠程序员自觉------记得加锁、记得解锁、记得正确的 memory order、记得不要把非原子变量用于多线程。Rust 通过类型系统把所有这些"记得"变成了编译器检查------
Mutex<T>绑定锁和数据、MutexGuard自动解锁、Ordering是枚举类型、原子类型与普通类型不兼容。