从C到Rust:Mutex / RwLock / Atomic 跨线程局部可变性

跨线程局部可变性 ------ Mutex / RwLock / Atomic

一、从单线程到多线程

1.1 Cell 和 RefCell 的线程限制

CellRefCell 只能在单线程中使用------它们不是 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 不能跨线程共享

这是因为 CellRefCell 内部没有使用原子操作,多线程同时访问会导致数据竞争。

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>

  • MutexGuardDrop 时自动解锁------不可能忘记 unlock

  • lock() 返回 Result<MutexGuard<T>, PoisonError>------如果其他线程在持有锁时 panic,Mutex 被标记为"毒化"(poisoned)

2.2 多线程共享 Mutex

Mutex 本身是 SendSync 的,所以可以通过 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 何时使用读写锁

当数据的读操作远多于写操作时,RwLockMutex 更高效------它允许多个读者同时访问:

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 是枚举类型、原子类型与普通类型不兼容。

相关推荐
程序员爱钓鱼3 小时前
Rust 注释与文档注释详解:从代码说明到 cargo doc
后端·rust
花褪残红青杏小14 小时前
Rust图像处理第18节-分形画布缩放 + 拖拽交互:图像缩放 + f32 性能优化
rust·webassembly·图形学
doiito15 小时前
【开源项目】用 Rust 重构中文 TTS!kokoroi-rs 高性能语音合成引擎介绍
ai·rust
铅笔侠_小龙虾16 小时前
Rust 学习目录
开发语言·学习·rust
铅笔侠_小龙虾1 天前
Rust 学习(4)-函数与注释
开发语言·学习·rust
前端H1 天前
Biome & Rolldown:Rust 工具链接管前端基建
开发语言·前端·rust
独孤留白1 天前
从C到Rust:struct Cell / RefCell 线程内局部可变性
rust
Kapaseker1 天前
Rust 是如何处理异常的
rust·kotlin
铅笔侠_小龙虾1 天前
Rust 学习(8)-切片类型
学习·rust·c#