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

从抢票事故到锁的原理:互斥、条件变量与生产消费模型(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 信号量、环形队列与线程池。

相关推荐
且从容.1 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落3 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨3 小时前
WSL2的安装与使用
linux·wsl
微小的鱼3 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng3 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮4 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky16 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默16 小时前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu
skywalk816317 小时前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd
爱莉希雅&&&19 小时前
GitLab 完整学习笔记
linux·运维·服务器