从抢票事故到锁的原理-互斥条件变量与生产消费模型

从抢票事故到锁的原理:互斥、条件变量与生产消费模型(8-16 ~ 8-19)

我的github:(https://github.com/xcx55/ubuntu-linux-project)

感谢各位大佬参观我的github!!

多线程共享地址空间的红利吃到之后,代价立刻就来了:多个执行流同时改同一份数据,不出事才怪。这几天的笔记沿着"出事 → 加锁 → 锁怎么实现 → 光锁不够 → 同步"一路推下来,正好一篇讲完。


一、事故现场:抢票为什么会出问题(8-16)

场景很简单:多个线程对同一个全局票数 --。笔记里的推演:

当一个线程条件判断进来了,后面切走、还没有减去;再来一个线程,再切走,也没有减去!这样一直来------直到放进来了很多个线程的 --,但票数没有得到保护!

本质原因:"判断 + 修改"是两步,线程可以在两步之间被切走。寄存器里的值还是旧票数,另一个线程已经把内存里的票减掉了------数据不一致就此发生。

结论:访问公共资源会出问题 → 必须把共享资源升级为临界资源 → 同一时刻只允许一个执行流进入。这就是互斥锁的动机。

加锁前先想清楚的三个原则问题

笔记里列的加锁原则,全是实战坑:

  1. Mutex 是共享资源,它保护别人,谁保护它? ------答案:lock/unlock 本身被设计为原子的(后面看实现);
  2. 一些线程加锁、一些不加锁,可以吗?------不行,不加锁的线程等于绕过了规则,锁形同虚设;
  3. 申请不成功怎么办?------阻塞等待,这也是加锁把"并行"变"串行"的代价。

锁的粒度因此成为核心权衡:加锁范围内的代码串行执行,效率会降------所以临界区要尽量小,"串行的时间变小",加锁的同时必须考虑时间效率。


二、锁的原理:一条交换指令定胜负(8-17)

1. 硬件方案:先看看但别用

硬件实现锁的思路简单粗暴:执行临界区代码的 PCB 不允许时钟切换 ------关闭时钟中断,自然没人能打断你。但关中断太危险(中断全关,系统响应瘫痪),一般不用。

2. 软件方案:xchgb 交换指令

软件实现的互斥锁,胜负手是一条汇编指令:

谁先拿到执行 xchgb 汇编,谁就拿到了锁!拿锁就一条汇编指令,没有三条指令!

为什么一条指令就够?回忆之前信号章的结论:CPU 的底层是一个汇编指令集,一条汇编语句是原子的 ------要么完整执行,要么不执行,不存在执行到一半被切走。而"检测锁变量 + 占有锁"这两个动作,被 xchgb(交换寄存器和内存的值)合成了一条指令:

  • 交换回来的是 0 → 别人已持有,继续等待;
  • 交换回来的是 1 → 锁到手,进临界区。

解锁更简单:直接硬编码把锁的值写回去------也是一条汇编指令。

3. 两个重要推论

  • 加锁之后可以随便切换调度!只要拿到锁的线程不执行"还锁"的函数、不把锁换回去,别的线程在锁上等待即可------锁保护的是临界区,不是禁用调度;
  • 把互斥锁理解为信号量 :互斥锁本质是对资源的预定机制------这句话为下一篇的 POSIX 信号量埋好了伏笔。

4. 全局锁用宏、局部锁用函数的原因

  • 全局(静态)分配的锁:用宏 PTHREAD_MUTEX_INITIALIZER 初始化;
  • 栈上开辟的锁:必须用 pthread_mutex_init 函数初始化。

笔记点破:因为那个宏不是普通的宏------它要在编译期就把锁置成"未持有"的确定状态,只有全局变量能保证;栈上变量的地址运行期才确定,只能走函数。


三、光有锁还不够:忙轮询的浪费(8-17 ~ 8-19)

1. 只有互斥会发生什么

生产消费场景里,消费者拿到锁 → 发现没数据 → 释放锁回到就绪队列 → 马上又参与竞争 → 又拿到 → 又没数据......

当前申请锁失败就会回到就绪队列忙轮询,没有等待队列的生成。你放了苹果,每次还要检测------大量的检测和获取(写入)只维护了互斥关系,效率降低,而且不合理!

8-19 那篇总结得最直白:

要是不要条件变量,线程就一直待在就绪队列等被调度,CPU 资源白白消耗 。不如让它等待------排好队、有序进行,这才是 CPU 算力的优化。

2. 同步的说人话定义

同步:在保证共享资源安全的情况下,让"拿钥匙的顺序"具有一定的顺序性。

在同步发明之前,互斥失败就回就绪队列------没有等待队列 。于是会出现"抢钥匙很强的线程无限次抢到钥匙,其他线程饿死"。同步就是用阻塞等待替代忙轮询:一个一个串行拿钥匙执行。


四、条件变量:铃铛 + 等待队列(8-18)

1. 模型

条件变量 = 一个等待队列 + 唤醒机制:

  • pthread_cond_wait(cond, mutex):线程挂入条件变量的等待队列(返回 0 成功、非 0 失败);
  • pthread_cond_signal :敲一下铃铛,唤醒队列里一个等待者;
  • pthread_cond_broadcast:广播,唤醒全部。

可以配两个铃铛------"队列空了等消费者敲、队列满了等生产者敲"(单同步 vs 多同步)。

2. pthread_cond_wait 为什么必须传锁

笔记里连续问了两次"为什么 wait 要传递锁",答案是两层:

  1. cond 的等待队列本身是共享资源------操作它必须持锁,所以 wait 必须在加锁之后调用;
  2. wait 内部要"换锁" :挂入等待队列的同时把持有的锁释放掉 ,被唤醒后再重新申请锁------要不然全部线程都要等待这个休眠的线程(你睡着了还抱着锁,别人永远进不来)。

3. if 改 while

被唤醒后条件可能又被别人抢了(广播唤醒一堆人、或唤醒后条件被消费),所以判断要用 while 再查一遍,不能用 if 只查一次。这是条件变量代码的第一铁律。

4. 321 原则:生产者消费者模型

  • 3 种关系:生产者×生产者(互斥)、消费者×消费者(互斥)、生产者×消费者(互斥 + 同步);
  • 2 种角色:生产者线程、消费者线程;
  • 1 个交易场所:阻塞队列("超市")。

模型的价值:解耦 (生产消费互不等待)、支持并发 (投递和拿取很快)、提升效率 ------运行任务不阻塞投递和拿取。笔记还点了一句:这个阻塞队列模型越看越像进程间通信里的管道------只是这次的阻塞是线程自己用条件变量实现的。


五、小结

问题 答案
为什么会数据不一致 "判断+修改"两步之间可被切走
锁的实现 xchgb 一条交换指令完成检测+占有,解锁一条指令写回
加锁后能切走吗 能;锁保护临界区,不禁用调度
全局锁宏 vs 局部锁函数 宏要求编译期确定的地址,只有全局变量满足
互斥 vs 同步 互斥管"一次一个",同步管"顺序合理"
条件变量本质 等待队列 + 铃铛(signal/broadcast)
wait 为什么传锁 等待队列是共享资源;wait 内部要放锁再睡

一句话:互斥解决"安全",同步解决"有序";锁是一条交换指令的原子性,条件变量是把忙轮询换成带铃铛的等待队列------CPU 的算力从"反复试探"变成了"精准唤醒"。

下一篇把资源从"整体使用"拆成"多份使用"------POSIX 信号量、环形队列与线程池。

相关推荐
AI+程序员在路上3 小时前
AP6275S蓝牙双接口解析:HCI UART与PCM的分工与协同
linux·c语言·物联网
小此方4 小时前
Linux网络(十八):TCP连接管理详解:从三次握手、四次挥手到CLOSE_WAIT与TIME_WAIT,深入理解2MSL与端口复用
linux·网络·网络协议·tcp/ip
进击的荆棘4 小时前
Linux系统——文件(上)
linux·运维·服务器·文件
k4m7v2pz4 小时前
ThinkPad E490 风扇 “转两秒就停“ 实录:迟滞曲线 + 最短运转时间
linux·脚本·archlinux·thinkpad·风扇·散热
拂拉氏4 小时前
【知识讲解】 Linux文件系统底层认识
linux·文件系统
IT大白鼠4 小时前
彭大帅的AI运维助手——自然语言管理 Linux 集群与网络设备——第 1 篇 · 骨架:说人话,跑命令:自然语言 SSH 运维的骨架
linux·运维·人工智能
牢姐与蒯4 小时前
Linux信号(一).信号产生
linux·运维·服务器·ubuntu
雯宝10 小时前
mnt_init 函数
linux
律宏阔16 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows