ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

从抢票事故到锁的原理-互斥条件变量与生产消费模型 从抢票事故到锁的原理互斥、条件变量与生产消费模型8-16 ~ 8-19我的github(https://github.com/xcx55/ubuntu-linux-project)感谢各位大佬参观我的github多线程共享地址空间的红利吃到之后代价立刻就来了多个执行流同时改同一份数据不出事才怪。这几天的笔记沿着出事 → 加锁 → 锁怎么实现 → 光锁不够 → 同步一路推下来正好一篇讲完。一、事故现场抢票为什么会出问题8-16场景很简单多个线程对同一个全局票数--。笔记里的推演当一个线程条件判断进来了后面切走、还没有减去再来一个线程再切走也没有减去这样一直来——直到放进来了很多个线程的--但票数没有得到保护本质原因判断 修改是两步线程可以在两步之间被切走。寄存器里的值还是旧票数另一个线程已经把内存里的票减掉了——数据不一致就此发生。结论访问公共资源会出问题 → 必须把共享资源升级为临界资源 → 同一时刻只允许一个执行流进入。这就是互斥锁的动机。加锁前先想清楚的三个原则问题笔记里列的加锁原则全是实战坑Mutex 是共享资源它保护别人谁保护它——答案lock/unlock 本身被设计为原子的后面看实现一些线程加锁、一些不加锁可以吗——不行不加锁的线程等于绕过了规则锁形同虚设申请不成功怎么办——阻塞等待这也是加锁把并行变串行的代价。锁的粒度因此成为核心权衡加锁范围内的代码串行执行效率会降——所以临界区要尽量小“串行的时间变小”加锁的同时必须考虑时间效率。二、锁的原理一条交换指令定胜负8-171. 硬件方案先看看但别用硬件实现锁的思路简单粗暴执行临界区代码的 PCB 不允许时钟切换——关闭时钟中断自然没人能打断你。但关中断太危险中断全关系统响应瘫痪一般不用。2. 软件方案xchgb 交换指令软件实现的互斥锁胜负手是一条汇编指令谁先拿到执行 xchgb 汇编谁就拿到了锁拿锁就一条汇编指令没有三条指令为什么一条指令就够回忆之前信号章的结论CPU 的底层是一个汇编指令集一条汇编语句是原子的——要么完整执行要么不执行不存在执行到一半被切走。而检测锁变量 占有锁这两个动作被 xchgb交换寄存器和内存的值合成了一条指令交换回来的是 0 → 别人已持有继续等待交换回来的是 1 → 锁到手进临界区。解锁更简单直接硬编码把锁的值写回去——也是一条汇编指令。3. 两个重要推论加锁之后可以随便切换调度只要拿到锁的线程不执行还锁的函数、不把锁换回去别的线程在锁上等待即可——锁保护的是临界区不是禁用调度把互斥锁理解为信号量互斥锁本质是对资源的预定机制——这句话为下一篇的 POSIX 信号量埋好了伏笔。4. 全局锁用宏、局部锁用函数的原因全局静态分配的锁用宏PTHREAD_MUTEX_INITIALIZER初始化栈上开辟的锁必须用pthread_mutex_init函数初始化。笔记点破因为那个宏不是普通的宏——它要在编译期就把锁置成未持有的确定状态只有全局变量能保证栈上变量的地址运行期才确定只能走函数。三、光有锁还不够忙轮询的浪费8-17 ~ 8-191. 只有互斥会发生什么生产消费场景里消费者拿到锁 → 发现没数据 →释放锁回到就绪队列→ 马上又参与竞争 → 又拿到 → 又没数据……当前申请锁失败就会回到就绪队列忙轮询没有等待队列的生成。你放了苹果每次还要检测——大量的检测和获取写入只维护了互斥关系效率降低而且不合理8-19 那篇总结得最直白要是不要条件变量线程就一直待在就绪队列等被调度CPU 资源白白消耗。不如让它等待——排好队、有序进行这才是 CPU 算力的优化。2. 同步的说人话定义同步在保证共享资源安全的情况下让拿钥匙的顺序具有一定的顺序性。在同步发明之前互斥失败就回就绪队列——没有等待队列。于是会出现抢钥匙很强的线程无限次抢到钥匙其他线程饿死。同步就是用阻塞等待替代忙轮询一个一个串行拿钥匙执行。四、条件变量铃铛 等待队列8-181. 模型条件变量 一个等待队列 唤醒机制pthread_cond_wait(cond, mutex)线程挂入条件变量的等待队列返回 0 成功、非 0 失败pthread_cond_signal敲一下铃铛唤醒队列里一个等待者pthread_cond_broadcast广播唤醒全部。可以配两个铃铛——“队列空了等消费者敲、队列满了等生产者敲”单同步 vs 多同步。2. pthread_cond_wait 为什么必须传锁笔记里连续问了两次为什么 wait 要传递锁答案是两层cond 的等待队列本身是共享资源——操作它必须持锁所以 wait 必须在加锁之后调用wait 内部要换锁挂入等待队列的同时把持有的锁释放掉被唤醒后再重新申请锁——要不然全部线程都要等待这个休眠的线程你睡着了还抱着锁别人永远进不来。3. if 改 while被唤醒后条件可能又被别人抢了广播唤醒一堆人、或唤醒后条件被消费所以判断要用 while 再查一遍不能用 if 只查一次。这是条件变量代码的第一铁律。4. 321 原则生产者消费者模型3 种关系生产者×生产者互斥、消费者×消费者互斥、生产者×消费者互斥 同步2 种角色生产者线程、消费者线程1 个交易场所阻塞队列“超市”。模型的价值解耦生产消费互不等待、支持并发投递和拿取很快、提升效率——运行任务不阻塞投递和拿取。笔记还点了一句这个阻塞队列模型越看越像进程间通信里的管道——只是这次的阻塞是线程自己用条件变量实现的。五、小结问题答案为什么会数据不一致判断修改两步之间可被切走锁的实现xchgb一条交换指令完成检测占有解锁一条指令写回加锁后能切走吗能锁保护临界区不禁用调度全局锁宏 vs 局部锁函数宏要求编译期确定的地址只有全局变量满足互斥 vs 同步互斥管一次一个同步管顺序合理条件变量本质等待队列 铃铛signal/broadcastwait 为什么传锁等待队列是共享资源wait 内部要放锁再睡一句话互斥解决安全同步解决有序锁是一条交换指令的原子性条件变量是把忙轮询换成带铃铛的等待队列——CPU 的算力从反复试探变成了精准唤醒。下一篇把资源从整体使用拆成多份使用——POSIX 信号量、环形队列与线程池。
返回列表