模块一:互斥量 (Mutex) ------ 保护共享数据的"锁"
核心痛点:多线程同时读写同一个变量会导致"数据竞争(Data Race)",引发程序崩溃或结果错乱。
1. C++ 的 Mutex:分离式设计(防君子不防小人)
- 设计哲学 :程序员对自己负责。锁 (
std::mutex) 和数据是完全分离的两个变量。 - 最佳实践 (RAII) :永远不要手动调用
m.lock()。必须使用std::lock_guard,它会在离开大括号作用域时自动解锁。 - 隐患 :如果你忘记写锁,直接修改数据,编译器绝对不会报错,但程序运行时会出大问题。
💻 C++ 代码示例:
#include <iostream>
#include <mutex>
#include <thread>
int counter = 0; // 数据(裸奔状态)
std::mutex mtx; // 锁(和数据毫无关联)
void add_count() {
// RAII 机制:lg 诞生时自动加锁,函数结束 lg 死亡时自动解锁
std::lock_guard<std::mutex> lg(mtx);
counter++; // 如果你这行代码写在 lg 上面,编译器不会管你,但会导致数据竞争!
}
2. Rust 的 Mutex:包裹式设计(强制安全)
- 设计哲学 :无畏并发(Fearless Concurrency)。数据必须放在锁的内部:
Mutex<T>。 - 绝对安全 :不调用
.lock()拿到钥匙,你连数据的影子都看不到,编译器在编译期彻底消灭了数据竞争。 - 多线程共享 :必须配合原子引用计数指针
Arc,组合为Arc<Mutex<T>>。
🦀 Rust 代码示例:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 数据 0 被死死锁在 Mutex 内部,外面包了一层 Arc 允许跨线程共享
let counter = Arc::new(Mutex::new(0));
let counter_clone = Arc::clone(&counter);
thread::spawn(move || {
// 必须调用 lock() 并 unwrap 才能拿到里面的数据引用
// num 离开作用域时,Rust 会自动解锁
let mut num = counter_clone.lock().unwrap();
*num += 1;
}).join().unwrap();
}
模块二:多线程与批处理 (Batch Processing)
核心概念:批处理是自动处理海量数据的任务模式。结合多线程可以榨干多核 CPU 性能。
1. 数据并行 (Data Parallelism) ------ 首选 rayon 库
这是 Rust 中最爽的批处理优化。把庞大的数组切块,丢给不同线程处理。
💻 代码对比:
// 传统的单线程批处理(慢)
data.iter_mut().for_each(|x| *x *= 2);
// 使用 Rayon 的多线程批处理(快到飞起)
use rayon::prelude::*;
data.par_iter_mut().for_each(|x| *x *= 2); // 仅仅多加了一个 par_!
2. 分发与聚合 (Map-Reduce 思想) ------ 避开锁竞争
避坑指南 :在批处理计算(如大范围求和)时,千万不要 让 10 个线程去抢一把 Mutex 锁来更新全局变量,频繁抢锁比单线程还慢!
正确做法:让每个子线程计算局部结果,最后由主线程汇总。
🦀 Rust 代码示例(无锁批处理):
use std::thread;
fn main() {
let mut handles = vec![];
// 分发 (Map):4 个线程各自计算一部分
for i in 0..4 {
let handle = thread::spawn(move || {
let mut local_sum = 0;
for j in 1..=1000 { local_sum += i * j; }
local_sum // 线程直接返回结果,不需要任何 Mutex!
});
handles.push(handle);
}
// 聚合 (Reduce):主线程收集结果
let mut total = 0;
for h in handles {
total += h.join().unwrap();
}
}
模块三:条件变量 (Condition Variable) ------ 线程间的"红绿灯"
核心痛点 :线程需要等待某个条件(如:有资源了)。写 while(true) 死循环会把 CPU 烧爆。
解决方案:条件变量。条件不满足就挂起休眠(不占 CPU),条件满足了由别人唤醒。
1. C++ 中的条件变量与信号量 (Semaphore)
- 唤醒策略 :优先使用
notify_all()(唤醒所有人去抢锁和重新检查条件),这是防止死锁、应对虚假唤醒的最安全策略。
💻 C++ 代码示例 (来自 PPT 的信号量实现):
(注:标准 C++ 中 cv.wait 必须用 unique_lock,不能用 lock_guard,因为等待时需要临时解锁)
void semaphore::wait() {
std::unique_lock<std::mutex> ul(m);
// 如果 value <= 0,就释放锁 m 并休眠;被唤醒后自动重新抢锁并检查条件
cv.wait(ul, [this] { return value > 0; });
value--;
}
void semaphore::signal() {
std::lock_guard<std::mutex> lg(m);
value++;
// 从 0 变成 1,说明可能有线程在死等,唤醒它们!
if (value == 1) cv.notify_all();
}
2. Rust 中的条件变量 (Condvar)
- 标准绑定 :永远与 Mutex 组成 CP:
Arc<(Mutex<T>, Condvar)>。 - 极致优雅的
wait()设计 :利用所有权转移,强制交出锁才能睡觉,醒来必须接收新锁。
🦀 Rust 代码示例:
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
fn main() {
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair_clone = Arc::clone(&pair);
thread::spawn(move || {
let (lock, cvar) = &*pair_clone;
let mut ready = lock.lock().unwrap(); // 拿到锁
// 应对虚假唤醒的标准 while 循环
while !*ready {
// ready(锁)交出去,线程休眠;
// 醒来后,wait 返回一个新的 MutexGuard 重新赋值给 ready
ready = cvar.wait(ready).unwrap();
}
println!("条件满足,开干!");
});
// 主线程唤醒逻辑...
let (lock, cvar) = &*pair;
*lock.lock().unwrap() = true; // 修改条件
cvar.notify_one(); // 唤醒子线程
}
模块四:Rust 语法扫盲 (扫清阅读代码的障碍)
今天重点攻克了两句让人懵逼的 Rust 并发代码,它们完美体现了 Rust 的严谨。
1. 神奇的符号组合:let (lock, cvar) = &*pair_clone;
let pair_clone: Arc<(Mutex<bool>, Condvar)> = ...;
let (lock, cvar) = &*pair_clone;
*(解引用) :看穿Arc玻璃展柜,目光锁定里面的元组。&(借用):我不拿走(不能 Move 共享数据),我只是隔着玻璃获取元组的引用。- 结果 :模式匹配自动将引用分配,
lock变成了&Mutex,cvar变成了&Condvar。
2. 词性撞车的命名:let mut ready = lock.lock().unwrap();
// 假设上一步得到的变量名不叫 lock,叫 my_mutex
let mut ready = my_mutex.lock().unwrap();
my_mutex/lock:点前面的词,只是一个变量名(名词)。.lock():点后面的词,是 Mutex 自带的方法(动词),作用是向 OS 申请加锁。ready:加锁成功后返回的智能通行证 (MutexGuard) 。利用它不仅可以修改内部数据,还能在它离开大括号作用域时触发 RAII 自动解锁。