多线程编程:整个互斥的流程以及scoped_lock的用法,以及作用,以及 硬件上的原子操作和逻辑上的原子操作

你的比喻简直神准 !在多线程的世界里,std::mutex(互斥量)最形象的对应物确实就是**"厕所门上的那把锁"**。

我们可以顺着你这个"厕所"的比喻,把这套逻辑彻底理清楚:


1. 核心概念对应表

现实世界 多线程编程 术语
厕所(资源) 一个全局变量(如 int money 临界资源 (Critical Resource)
想要上厕所的人 不同的线程 线程 (Thread)
门上的那把锁 std::mutex mtx; 互斥量 (Mutex)
锁门 / 开门 mtx.lock() / mtx.unlock() 加锁 / 解锁

2. 为什么需要这把"锁"?

如果没有这把锁(Mutex),就会发生**"惨剧"**:

  1. 张三(线程 A)进去了。
  2. 李四(线程 B)发现门没锁,也冲进去了。
  3. 结果 :厕所里的数据(资源)会被两个人同时改动,导致程序崩溃或逻辑混乱(也就是所谓的竞态条件 Race Condition)。

3. std::mutex 的正确用法

虽然 mutexlock()unlock() 方法,但就像我们之前聊过的,手动开关锁很危险(万一你在厕所里晕倒了/程序抛异常了,没法开门,外面的人就永远进不去了)。

所以我们通常用 "自动管理员" 来操作这把锁:

cpp 复制代码
#include <mutex>

std::mutex mtx_gold; // 定义这把锁
int gold_count = 0;  // 这是那个"厕所"(受保护的资源)

void add_gold() {
    // 使用 std::scoped_lock(自动管理员)
    // 只要这一行代码执行,就像是进厕所顺手把门反锁了
    std::scoped_lock lock(mtx_gold); 
    
    gold_count++; // 安全地操作资源
    
} // 函数结束,lock 变量销毁,门自动打开(解锁)

4. 深入一步:Mutex 保护的是什么?

这是一个常见的误区:std::mutex 并不自动绑定某个变量。

  • 编译器并不知道 mtx_gold 是保护 gold_count 的。
  • 这全靠程序员的自觉 : "只要我想动 gold_count,我就必须先去拿 mtx_gold 这把钥匙。"

如果你在另一个函数里直接修改 gold_count 而不加锁,那这把锁就形同虚设了。这就像是有个莽撞的人不走门,直接翻窗进厕所一样。


5. 总结:你提到的几个概念是怎么串起来的?

现在我们可以把你问的所有东西串成一个完整的流程图

  1. std::mutex:定义那把锁。
  2. std::scoped_lock:安全地、成套地去拿锁(防止死锁)。
  3. std::condition_variable (wait/notify)
    • 如果厕所里没纸了(条件不满足),你就先出来在走廊椅子上睡一会(wait)。
    • 等保洁阿姨放了纸,她喊一嗓子(notify_all),把你叫醒。
    • 你醒了之后,第一件事还是得去抢那把锁(Mutex),抢到了才能再进厕所。

如果没有std::scoped_lock 来帮你实现互斥自动上锁解锁

如果不用 C++17 推荐的 std::scoped_lock,传统的"普通"写法通常有两种:一种是手动挡 (容易出事),另一种是经典自动挡(C++17 之前的标准做法)。

顺着咱们的"厕所"比喻,我们来看看代码怎么写:


1. 手动挡:lock()unlock()

这是最原始的写法,就像你进厕所手动插销,出来手动拔销。

cpp 复制代码
void add_gold() {
    mtx_gold.lock();   // 手动锁门
    
    gold_count++;      // 操作资源
    
    mtx_gold.unlock(); // 手动开门
}

⚠️ 这种写法的"大坑":

  • 忘记解锁 :如果你在 gold_count++ 后面写了一堆复杂的逻辑,最后忘了写 unlock(),那这个厕所就永远被锁死了,别人(其他线程)等到憋死也进不去。
  • 异常崩溃 :如果 gold_count++ 这一行抛出了异常,程序会直接跳出函数,跳过了 unlock()。结果还是一样:死锁。

2. 经典自动挡:std::lock_guard

在 C++17 的 std::scoped_lock 出现之前,这是最常用的"普通"写法。它利用了 RAII 机制(资源获取即初始化)。

cpp 复制代码
void add_gold() {
    // 经典的自动管理员
    std::lock_guard<std::mutex> lock(mtx_gold); 
    
    gold_count++; 
    
} // 函数结束,lock 变量自动销毁,顺便把门开了

它和 std::scoped_lock 的区别:

  • std::lock_guard:只能管一把锁。如果你想同时锁两个互斥量,它就无能为力了(容易导致死锁)。
  • std::scoped_lock:可以管一把或多把锁 。它是 lock_guard 的增强版。

3. 灵活自动挡:std::unique_lock

这是另一种"普通"写法,但它比上面两个都要 ,也更灵活

cpp 复制代码
void add_gold() {
    std::unique_lock<std::mutex> lock(mtx_gold);
    
    gold_count++;
    
    // 它允许你在函数没结束时,手动提前解锁
    lock.unlock(); 
    
    // 做点别的不需要锁的事情...
    
    // 甚至之后还能再锁上
    lock.lock();
}

什么时候用它? 配合我们最开始聊的 std::condition_variable(条件变量)时,必须 使用 std::unique_lock,因为条件变量在 wait 的时候需要频繁地自动开启和关闭锁。


总结对比表

工具 对应现实场景 推荐程度 备注
mtx.lock/unlock 手动插销/拔销 ⭐ (不推荐) 极易忘解锁,导致死锁
std::lock_guard 弹簧门(进门锁,出门开) ⭐⭐⭐ C++11 经典款,单锁首选
std::unique_lock 智能锁(能自动也能手动) ⭐⭐⭐⭐ 配合 wait() 必选,功能最全
std::scoped_lock 万能管家 ⭐⭐⭐⭐⭐ 现代 C++ 最佳实践,防死锁神方

既然提到了手动解锁的风险,你想看看如果不小心造成了"死锁",在调试器里看起来是什么样子的吗?

std::scoped_lock 他其实也就跟原子操作似的,整个上锁解锁的流程是不允许打断的呗

你的理解非常有灵性!你抓住了**"事务性"或者说"全有或全无"**这个核心点。

不过,从底层技术上来说,我们需要把**"操作原子性""逻辑原子性"**稍微区分一下,这样你对多线程的理解会更上一层楼:


1. 它是"逻辑上"的原子操作

你说得对,std::scoped_lock 的核心目标就是:要么我把这几把锁全都锁上,要么我一把都不占。

如果它在尝试锁住 mtx_Amtx_Bmtx_C 的过程中,发现 mtx_C 被别人占了,它不会像憨憨一样拿着 AB 死等,而是会:

  1. 释放已经拿到的 AB
  2. 稍微等一下或者换个顺序重试。
  3. 直到它能一次性把三个都拿齐。

这种"一次性拿齐"的效果,确实就像原子操作一样,外界看不到它"只锁了一半"的中间状态。


2. "不允许打断"指的是什么?

这里有个微妙的区别,很多初学者会搞混:

  • OS 层的打断(线程切换)

    哪怕你用了 scoped_lock 锁住了资源,操作系统依然可以打断这个线程 (比如分给你的 CPU 时间片用完了)。
    但是! 即使你的线程被操作系统踢到后台去休息了,那把锁(Mutex)依然在你手里拎着 。别的线程过来一看,门还是锁着的,它们依然进不去。
    所以,从"保护资源"的角度看,它是不可打断的。

  • 硬件层的原子操作 (std::atomic)

    这才是真正的"物理不可打断"。比如 gold_count++ 如果是原子变量,CPU 执行它时是一条指令,在硬件层面保证了这期间绝对不会有任何切换。


3. 一个直观的对比

为了让你彻底明白 scoped_lock 和真正的"原子变量"的区别,看这个例子:

特性 std::atomic<int> count (原子变量) std::mutex + scoped_lock (锁机制)
比喻 像一个自动售货机:投币、出货一气呵成,没法拆分。 像一个带锁的仓库:你得先拿钥匙开门,进去搬货,再出来锁门。
范围 只能保护一个变量。 可以保护一整段代码逻辑 ,或者多个变量。
被打断 物理上不可拆分,操作瞬间完成。 过程中可能被 OS 切换,但没人能进仓库干扰你。
开销 极低(硬件支持)。 较高(涉及系统调用和线程排队)。

4. 为什么 scoped_lock 这么厉害?

它最牛的地方在于解决多资源分配

想象一下:你要从账户 A 转账到 账户 B。这需要同时锁住 mtx_Amtx_B

  • 如果不用 scoped_lock,你可能刚锁住 A,还没锁 B,就被打断了。
  • scoped_lock(mtx_A, mtx_B) 给了你一种**"我只要出场,这两个账户就同时归我管"**的霸气感。

💡 深度总结

你的直觉是对的:std::scoped_lock 确实利用了某种"算法上的原子性"来保证多把锁的安全性。 它让我们写代码时,可以把"获取多个资源"看作一个不可分割的动作,从而完美避开死锁。

相关推荐
小宇的天下2 小时前
Calibre LVS Circuit Comparison(3)
开发语言·php·lvs
liuyao_xianhui2 小时前
优选算法_topk问题_快速排序算法_堆_C++
java·开发语言·数据结构·c++·算法·链表·排序算法
yunn_2 小时前
Qt智能指针
c++·qt
liuyao_xianhui2 小时前
优选算法_堆_最后一块石头的重量_C++
java·开发语言·c++·算法·链表
上天_去_做颗惺星 EVE_BLUE2 小时前
Linux Core Dump 测试操作手册
linux·c++·测试工具
羊小猪~~2 小时前
算法/力扣--栈与队列经典题目
开发语言·c++·后端·考研·算法·leetcode·职场和发展
Noushiki2 小时前
数据一致性保障方案 -java后端
java·开发语言
书到用时方恨少!2 小时前
Python 零基础入门系列(终篇):综合实战项目
开发语言·python
扶摇接北海1762 小时前
洛谷:P1035 [NOIP 2002 普及组] 级数求和
算法