【加锁 】

文章目录

    • 锁 理论部分
    • 锁的原理
    • 锁的应用 --- 封装

锁 理论部分

在这里插入图片描述

定义锁的两种方案
1.定义全局锁
直接在全局用 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
就不用再 init 和 destroy 了。
2.定义局部锁
pthread_mutex_init
pthread_mutex_t 是库提供的一种数据类型
第二个形参是锁的属性,我们不关心直接Null

我们要把对应的资源保护起来,要保证多个线程申请的是同一把锁

共享变量加上锁保护后就叫临界资源
线程中不是每行代码都在访问临界资源
而是这一小块代码在访问临界资源tickets,我们把这一小块临界资源的代码叫做临界区
在这里插入图片描述
加锁是不是一件好事呢?
根据对互斥的学习,任何时刻只允许一个线程去访问临界资源
加锁的本质其实是对被加锁的代码区域让多线程串行访问

加锁不是一个特别好的事情,
加锁的本质: 是用时间来换安全
加锁的表现: 线程对于临界区代码串行执行
加锁原则: 尽量的要保证临界区代码,越少越好!

在循环外面加锁行不行?
在这里插入图片描述

1.从逻辑上不对,因为你难道想让一个线程在while循环中,一个线程把票全抢完然后才能到下一个线程抢吗,下一个线程抢都没票了。
2.而且临界区限定的区域让代码越少越好,临界区代码越少意味着在临界区执行时间越短,越短串行的比率就会降低,更重要的是在临界区里线程也可以被调度的话,临界区代码越短线程被调度的概率也就越低,进而让其他线程等的时间也就变短了

所以这样写是不对的


抢票时每个人先申请锁,谁拿到了锁谁才能进入临界区访问抢票
没有拿到锁的线程,就必须要在指定的锁上等待
申请锁成功,才能往后执行,不成功,阻塞等待。
表现形式就是在pthread_mutex_lock函数这里就卡住了
线程申请锁失败了,线程阻塞等待的本质是线程等待锁资源不就绪,调度器把线程PCB阻塞后去锁的队列里进行等待
在这里插入图片描述
但是这里还有问题,可能一个线程抢到锁了,在抢票时发现票没了直接break
此时解锁代码没有被执行,这个线程已经走了,但是锁一直没释放,其他线程会一直阻塞


正确写法
在这里插入图片描述

验证一下发现所有的票几乎被一个线程都抢走了,一会说这个问题。
但确实解决了抢到负数的问题
在这里插入图片描述
但是这样也不明显,我们再加上一句usleep(13)
在这里插入图片描述

在这里插入图片描述
这里面有非常多细节
刚刚没有usleep,发现一个锁一直被一个线程抢,正常吗?
在这里插入图片描述
正常,每一个线程抢票时,它一释放锁立马循环回去申请锁
线程对于锁的竞争能力可能会不同
因为你离锁最近,别的线程还没来及唤醒呢,唤醒其他线程的成本比持有锁的线程直接再去抢高,他一释放就去抢其他线程可能还要跑很多代码才能抢到

那为什么加了usleep就可以了呢?
在这里插入图片描述
因为一加usleep 这个锁你释放了先别着急申请锁你先等一等
你usleep期间其他线程就有机会拿到锁然后申请了
说明对于锁的访问一定是并发的,只不过因为一个线程竞争能力太强导致其他人抢不到

这份代码其实不太符合逻辑,我们抢到了票,我们会立马抢下一张吗?
不是
因为多线程抢到之后还有后续的工作比如把票信息插入你的名下
所以就用usleep来模拟后续的代码

纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题!
但不是说只要有互斥,必有饥饿。适合纯互斥的场景,就用互斥

如果把抢票逻辑写完全了,纯互斥就够了

那该怎么解决饥饿呢?
在这里插入图片描述

1.外面来的,必须排队
假如外面有100个线程,如果持有锁线程释放了,如果外面线程不排队那么OS要唤醒
100个线程全去抢锁,但只有一把锁,也就是说99个都是无效唤醒,这不合理
所以要排队

2.出来的人,不能立马重新申请锁,必须排到队列的尾部

让所有的线程(人)获取锁(钥匙),按照一定的顺序。按照一定的顺序性获取资源—同步!!
不一定非得按照队列,爱是什么结构是什么,只要有顺序性


锁本身就是共享资源 ! !
所以,申请锁和释放锁本身就被设计成为了原子性操作(如何做到的?)
不用担心申请锁多线程并发申请的问题,有且只有一个线程能得到锁


在临界区中,线程可以被切换吗???
肯定可以被切换

tickets- - 三行代码都可以切换,这临界区那一大坨代码,线程在任何地方都可以去切换
那切换了锁怎么办会不会出问题呢?
当线程在临界区中在任何地方都会被切换,但是当前线程不怕被切换,因为线程已经申请了锁并且并没有解锁,所以在线程被切出去的时候,是持有锁被切走的。
即便我不在期间,我只要没执行解锁,任何线程都进不去临界区访问临界资源。

===============================================================

我们今天为什么要加锁呢?
因为我们有并发问题
那为什么有并发问题呢?
因为我们使用多线程访问了全局变量
为什么要有多线程呢?
因为我们想提高代码的并发度而且不想创建进程那么重而是通过线程的方式简单创建

把线逆向推回去

为了提高并发度且要降低成本使用多线程
使用多线程就有线程间资源共享,虽然解决了并发度问题,但引入了多线程访问数据不一致问题
为了解决数据不一致所以引入了互斥锁,互斥锁使用时就要考虑临界资源,临界区,原子性等
单纯互斥锁还可能引发饥饿问题。

任何解决方案伴随着新的问题,所以这个世界上没有放之四海而皆准的法则,没有一套固有的一套方式能够把任何问题全部解决的。看看这个问题会不会影响我们最看重的,如果不关心那就不管了。

锁的原理

原子性的概念就是一件事情要么做了,要么没做,它没有中间状态
原子性可能存在很多场景

我们认为一条汇编语句已经是计算机里执行的最基本的指令了
今天得给原子这个概念下个定义,我们认为一条汇编语句就是原子的!
调度器调度时,一条代码是不会被CPU中断的,要么执行完了,要么不执行

下面谈一谈互斥锁是如何实现的

为了实现互斥锁操作,大多数体系结构都提供了swap or exchange指令
(大部分体系结构就是大部分CPU架构,CPU中有自己的指令集,就是内置了最基本的指令)
该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性
结论
体系结构提供了一条汇编指令exchange 把寄存器里的值和内存里的值做交换
因为是一条汇编,所以是原子的。

下面来看一段加锁和解锁的伪代码
在这里插入图片描述

lock 对应的就是 pthread_mutex_lock();

对于锁呢不用理解的太复杂,我们看来锁就是一个简单的变量,
定义一把锁,定义成局部或全局的其实就是定义对象
我们目前把锁当成一个内存里的整数 int mutexi = 1 表示锁存在

CPU里有eax 和 al寄存器 他们俩当成一个就行了
在这里插入图片描述

首先一个线程进来要执行伪代码第一句,把mov 把al寄存器清零
第二件事,把al寄存器里的内容和mutex变量里的内容交换
这个动作其实就是申请锁的动作
做完交换后他就拿到锁了,接下来就判断寄存器al里的值是不是大于0
如果是,return 0 代表申请锁成功,
如果不大于0,也就是申请锁失败 当前线程要进行等待。

上面是正常情况,下面说说特殊情况

当前是一个线程在执行代码哦
我的问题是,有没有可能线程1 刚把寄存器al置零了,这个线程就被切换了
线程2 也来申请锁了
在代码的任何地方被切换都是有可能的,别告诉我你现在在加锁
加锁函数实现里这么多汇编语句,上次讲tickets - - 时你都说任何地方都能切换,所以刚执行完置零线程被切换是有可能的

CPU的寄存器只有一套,但CPU的寄存器里的内容是每一个线程都要有一个自己的内容的
寄存器 != 寄存器内容
线程1要被切换走的时候必须把自己的上下文带走,包括al寄存器里的0
并且还要记录下来当前执行到哪里了方便继续往后执行xchgb交换
所以线程1 就走了
线程2 来了,假设线程2 非常顺利不会被别人所中断
它也要把寄存器al写0,实际上是写到了线程2 的硬件上下文中
线程2 要xchgb把寄存器al的值和内存mutex交换 al是1
线程2 继续要判断寄存的内容,正准备判断时线程2 被切换走了
它走的时候要把寄存器里内容全部带走,也就是把al 是 1 带走,并且记录他走到 i f 位置
在这里插入图片描述

所以线程2 就被切换走了
然后把线程1 拿回来,它回来的时候 首先要恢复曾经带走的上下文,也就是al寄存器被恢复为0
然后继续把寄存器al的值和内存mutex交换,交换后线程1的al还是0,判断后直接走else挂机等待
线程1申请锁失败,线程1不会被调度了因为他被阻塞了,
所以线程2回来了,它也要先恢复自己的上下文,然后执行if 判断,发现al 是1 大于0,return申请锁成功
在这里插入图片描述

伪代码最核心的其实是xchgb交换
交换的本质是什么呢?
把内存中的数据(就是这把锁本质被所有线程共享),(线程执行)交换到CPU的寄存器中
本质是把数据交换到线程的硬件上下文中!!
线程的上下文是线程私有的!
所以把内存中的数据交换到寄存器里,本质是把一个共享的锁,让一个线程以一条汇编的方式,交换
到自己的上下文中!
一旦交换到自己的上下文中,所有线程申请锁都是用0和内存交换的,整个交换中1只有1个
交换到自己的上下文中,最终代表当前线程持有锁了!

最开始mutex变量是1,所有人都和我换,看起来是换到寄存器硬件了,但其实是换到了当前执行线程的上下文中,一旦换到它里面它线程一旦被切走它自己的上下文就保护了,其他线程也看不到 , mutex变量也是0 ,其他人也申请不到了

所以1就在内存和线程的上下文之间,1就跟令牌一样在多个线程内自由流动
所以竞争锁本质就是看谁运气好,更快的把exchange指令做完

至此就完成了加锁

再谈谈解锁
解锁的时候原子性重要吗?

在这里插入图片描述
mov就一条汇编 把mutex变量置1 是原子的

我们想的应该是线程里上下文的锁再交换回内存中Mutex变量,意味着加锁解锁必须是同一个线程
这里的unlock可以是其他线程来解锁

在编码上来说可以让其中某一个线程不加锁直接访问临界资源,其他线程加锁遵守互斥规则
但是这是有问题的,一旦有了互斥规则所有线程都应该遵守

锁的应用 — 封装

#pragma once#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t* lock):lock_(lock){}void lock(){pthread_mutex_lock(lock_);}void unlock(){pthread_mutex_unlock(lock_);}~Mutex(){}
private:pthread_mutex_t* lock_;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* lock):mutex_(lock){mutex_.lock();}~LockGuard(){mutex_.unlock();}
private:Mutex mutex_;
};

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/277125.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker 学习不再难:这些网站让你轻松掌握容器技术!

介绍&#xff1a;Docker是一个开源的应用容器引擎&#xff0c;基于Go语言并遵从Apache2.0协议开源。它可以让开发者将他们的应用及依赖打包到一个轻量级、可移植的容器中&#xff0c;进而发布到任何流行的Linux或Windows操作系统的机器上&#xff0c;同时也可以实现虚拟化。 Do…

certum ev ssl证书1180元一年,360浏览器显示公司名

Certum旗下的EV SSL证书是审核最严的数字证书&#xff0c;不仅对网站传输数据进行加密&#xff0c;还可以对网站身份进行验证&#xff0c;除此之外&#xff0c;它独有的绿色地址栏提升了网站的真实性&#xff0c;增强了客户对网站的信任感。今天就随SSL盾小编了解Certum旗下的E…

中国制造MES市场分析

据了解&#xff0c;作为制造业企业数字化转型的核心&#xff0c;制造执行系统是打通IT和OT&#xff0c;将运营和生产数据融合的关键通道。在工业元宇宙、数字孪生、智能制造、工业4.0、CPS等概念之下&#xff0c;MES在务实的制造业中需求明确、价值清晰&#xff0c;是制造业企业…

关东升老师从小白到大牛系列丛书(由清华大学出版社出版)

助力技术成长&#xff0c;成就大牛之路 在这个科技日新月异的时代&#xff0c;掌握一门编程语言或专业技能已是必备&#xff0c;不再是奢侈。清华大学出版社出版的“从小白到大牛”的系列丛书&#xff0c;涵盖Python、Java、Kotlin、Android和SQL&#xff0c;助你快速在技术之…

前端自定义icon的方法(Vue项目)

第一步&#xff1a;进入在线的编辑器进行设计 好用&#xff1a;百度字体编辑器 比如先导入有个ttf文件 添加新字体 双击每个模块进入编辑区域 更改相应的信息&#xff0c;比如name 编辑完了进行导出文件(各种格式就行了)就行了 第二步&#xff1a;在项目中asset文件储存这些文…

当你打开终端并输入命令时会发生什么?(下)

哈喽大家好&#xff0c;我是咸鱼 我们先来大致回顾一下文章《当你打开终端并输入命令时会发生什么?&#xff08;上&#xff09;》的内容 终端设备是由电传打字机演变过来的&#xff0c;电传打字机通过物理线与大型计算机连接在一块来实现输入输出 如上图&#xff0c;分别是二…

vue el-cascader组件change失效以及下拉框不消失的问题

文章目录 1.前言2. 碰到的问题3. 如何解决这两个问题 1.前言 最近项目上用到el-cascader这个组件,需要可以选第一级菜单&#xff0c;也需要可以选第二级菜单&#xff0c;点击完成之后需要关闭下拉框。其实功能比较简单&#xff0c;找了很多资料&#xff0c;没有找到合适的方案…

三层交换,DHCP的详解与VRRP

目录 一、三层交换 1、三层交换机的作用&#xff1a; 2.vlan的虚拟接口vlanif&#xff08;ifinterface接口&#xff09; 3.三层交换机实验 4.拓展实验​编辑 二、DHCP 1.自动获取ip地址&#xff1a; 2.DHCP的好处&#xff1a; 3.分配方式&#xff1a; 4.举例&#xff…

Redis 过期删除策略、内存回收策略、单线程理解

不知从何开始Redis的内存淘汰策略也开始被人问及&#xff0c;卷&#xff01;真的是太卷了。难不成要我们去阅读Redis源码吗&#xff0c;其实问题的答案&#xff0c;在Redis中的配置文件中全有&#xff0c;不需要你阅读源码、这个东西就是个老八股&#xff0c;估计问这个东西是想…

代码随想录算法训练营Day2 | 977.有序数组的平方、209.长度最小的子数组、59.螺旋矩阵II

LeetCode 977 有序数组的平方 本题思路&#xff1a;最容易想到的就是使用暴力循环的方式&#xff0c;将数组每个值都平方&#xff0c;然后进行一个排序操作。但是这样做&#xff0c;使用快排&#xff0c;它的复杂度也是 ologn。 所以&#xff0c;我们可以尝试用双指针的方法 &…

构建智能预约系统小程序:技术实现详解

随着移动互联网的发展&#xff0c;预约上门系统小程序成为服务行业中的一项创新解决方案。在这篇文章中&#xff0c;我们将深入研究如何使用技术构建一个强大而高效的预约上门系统小程序&#xff0c;并为你提供详细的技术实现步骤。 1. 开发环境准备 首先&#xff0c;确保你…

离散型概率密度函数的分布列⇔分布函数

目录 一、super误区 1.分布函数的定义 二、分布列⇒分布函数 二、分布列⇐分布函数 一、super误区 我在读定义的时候陷入了一个误区&#xff0c;与大家分享一下。 1.分布函数的定义 由于是离散型的概率密度函数&#xff0c;我把他抽象到数轴上理解&#xff1a; 如下分布…