深入并发编程:从 C++ 到 Rust 的学习笔记

模块一:互斥量 (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 变成了 &Mutexcvar 变成了 &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 自动解锁
相关推荐
WYT王玉桐2 小时前
软件测试(黑马)
学习
chushiyunen2 小时前
float浮点数计算-原理笔记
笔记
2201_754864782 小时前
学习日记 – 2026年4月2日
学习
A923A2 小时前
【小兔鲜电商前台 | 项目笔记】第二天
前端·vue.js·笔记·项目·小兔鲜
小CC吃豆子2 小时前
C/C++中 int 的最大最小值
c语言·开发语言·c++
bukeyiwanshui2 小时前
2026.4.2随堂笔记
笔记
欧米欧2 小时前
C++模板初阶
开发语言·c++
952362 小时前
计算机组成原理 - 主存储器
单片机·嵌入式硬件·学习·fpga开发
Kk.08022 小时前
数据结构|排序算法(二) 希尔排序
数据结构·算法·排序算法