013 Linux_互斥

前言

本文将会向你介绍互斥的概念,如何加锁与解锁,互斥锁的底层原理是什么

线程ID及其地址空间布局

在这里插入图片描述

每个线程拥有独立的线程上下文:一个唯一的整数线程ID, 独立的栈和栈指针,程序计数器,通用的寄存器和条件码。
和其他线程共享的进程上下文的剩余部分:整个用户虚拟地址空间,那就是上图的数据段,堆以及所有的共享库代码和数据区域,也共享所有打开文件的集合。
pthread_create函数会产生一个线程id,存放到第一个参数指向的地址中,如果你将这个id打印出来会发现特别大,其实这串数字是一个地址,这个地址就是一个虚拟地址,这样在主线程产生的临时数据都压在系统栈上,而其他线程则存储在pthread库提供的栈内。
这里要注意的是线程的寄存器的内容是不共享的,通常栈区是被相应线程独立访问的,但是还是可能出现一个线程去访问另一个线程中的栈区的情况。如果这个线程获得了指向另一个线程栈区的指针,那么它就可以读写这个栈的任何部分。

互斥相关概念

临界资源:多线程执行流共享的资源就叫做临界资源 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量: 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之 间的交互。
多个线程并发的操作共享变量,会带来一些问题。

模拟抢票

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 4
class threadData
{
public://构造函数threadData(int number){threadname = "thread-" + to_string(number);}
public:string threadname;      //线程名
};
int tickets = 1000;
void* getTicket(void *args)
{//安全类型转换threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while(tickets){if(tickets > 0){     usleep(10000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--; }else break;}printf("%s ... quit\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData *> thread_datas;for(int i = 1; i <= NUM; i++){pthread_t tid;//构造一个对象指针threadData *td = new threadData(i);//存放对象指针thread_datas.push_back(td);//创建线程,将对象指针作为参数传递给getTicketpthread_create(&tid, nullptr, getTicket, thread_datas[i-1]);//管理线程idtids.push_back(tid);}//线程等待for(auto thread : tids){pthread_join(thread, nullptr);}for(auto td : thread_datas){delete td;}return 0;
}

在这里插入图片描述

现象:明明存在对tickets的条件判断,可是票数依旧被抢到了负数

在这里插入图片描述

原因是:多个线程可能同时检查tickets大于0,然后同时减少tickets的值。这可能导致tickets的值减少到负数,因为每个线程都可以执行tickets–操作,即使tickets已经为0。
从底层来看:
Ticket–这一步骤在汇编上是三条代码

1、先将tickets读入到cpu的寄存器中
2、cpu内部进行–操作
3、将计算结果的数据写回内存

举个例子
在这里插入图片描述
引入一个概念:线程在执行的时候,将共享数据,加载到CPU寄存器的本质:把数据的内容,变成了自己的上下文—以拷贝的方式,给自己单独拿了一份(也就是说会将数据保存到自己的上下文当中)

1、当执行线程一的时候,倘若线程一刚执行完第一步就被切走
2、假设线程2一直能执行完3个步骤,重复执行,没有被中断,最终票数被减到10了,当刚要执行第一步骤的时候此时被切换,此时将减到10的数据保存到自己的上下文当中
3、切换到线程一,并不是紧接着执行第二步,而是恢复自己的上下文数据1000到CPU当中,它认为数据是1000,最后将计算出来的999写回内存,此时线程二先前将票数减到10的工作白做了
4、而此时如果再次切换到线程二,线程二再将内存中的tickets读取到寄存器当中(票数就又变成了999),因此可以看出:多个线程并发的操作共享变量,会带来一些问题,是线程切换导致的
我们也可以看出无论是–还是++其实都不是原子性的,因为它们会在cpu调度是被打断。

互斥锁

为了解决上述抢票的问题(共享数据被读哦现场并发访问造成数据不一致的问题)
需要做到三点:

代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
在这里插入图片描述

加锁:模拟抢票

引入函数接口

互斥锁
方法1,静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2,动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:NULL

销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码

调用 pthread_ lock 时,可能会遇到以下情况: 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。


#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 4class threadData
{
public://构造函数threadData(int number, pthread_mutex_t *mutex){threadname = "thread-" + to_string(number);lock = mutex;}
public:string threadname;      //线程名pthread_mutex_t *lock;
};
int tickets = 500;
void* getTicket(void *args)
{//安全类型转换threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while(true){//上锁pthread_mutex_lock(td->lock); if(tickets > 0){        usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--; pthread_mutex_unlock(td->lock);}else{//解锁pthread_mutex_unlock(td->lock);break;}}printf("%s ... quit\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData *> thread_datas;pthread_mutex_t lock;//初始化锁pthread_mutex_init(&lock, nullptr);for(int i = 1; i <= NUM; i++){pthread_t tid;//构造一个对象指针threadData *td = new threadData(i, &lock);//存放对象指针thread_datas.push_back(td);//创建线程,将对象指针作为参数传递给getTicketpthread_create(&tid, nullptr, getTicket, thread_datas[i-1]);//管理线程idtids.push_back(tid);}//线程等待for(auto thread : tids){pthread_join(thread, nullptr);}for(auto td : thread_datas){delete td;}return 0;
}

现象:上锁了,但是票数都是由一个线程抢走了
在这里插入图片描述
为什么会有这样的现象呢?
故事时间

在纯互斥环境里,如果锁分配不够合理,容易导致其它线程的饥饿问题
比如:存在一个独立自习室,规矩:出去后,必须把钥匙放到指定的位置。倘若自习室里的人需要去吃饭,然后出门,看到一大堆人在等他出来,然后他又进去了,因为他不想失去这把🔑,但是由于他距离门更近一些,因此他对钥匙的竞争更强一些
我们应该对此再加一个规矩:出来的人,不能立马重新申请锁,想要继续申请,必须排到队列的最后面,外面来的,必须排队

了解上述的故事之后,我们可以对代码进行如下修改:
在这里插入图片描述

在这里插入图片描述

互斥量实现原理探究

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下
在这里插入图片描述
1、当线程切换时,线程要把它的上下文数据带走(即把在寄存器中的值拷贝一份),还要记录执行到哪一个位置了
2、当线程一执行完第一步就被切换,首先把0保存到自己的上下文当中,回来的时候要执行xchgb
3、 线程二来了:把0mov到寄存器里,让后与内存中的mutex=1作交换(此时内存中的值为0,cpu寄存器的值为1)当正要做判断的时候,被切换了,线程二要把寄存器中的内容带走,并记录即将执行if语句
4、线程一回来了:首先要恢复上下文数据,将0又恢复到寄存器里,然后执行交换,发现 跟内存交换完后,依旧是0
5、原因是线程二已经拿走了1,线程一申请锁失败,不会被调度,线程二再恢复寄存器中的数据,继续进行判断大于0,申请锁成功
锁本身就是共享资源,放在内存里这个数据(仅有一把锁)就是被所有线程共享的

在这里插入图片描述
此时持有锁的线程再将mutex中的值(此时为0)与1交换
在这里插入图片描述

小结

今日的分享就到这里啦,如果本文存在疏漏或错误的地方,还请您能够指出!
在这里插入图片描述

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

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

相关文章

【C++】深度解剖多态

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;了解什么是多态&#xff0c;熟练掌握多态的定义&a…

【SpringCloud】微服务重点解析

微服务重点解析 1. Spring Cloud 组件有哪些&#xff1f; 2. 服务注册和发现是什么意思&#xff1f;Spring Cloud 如何实现服务注册和发现的&#xff1f; 如果写过微服务项目&#xff0c;可以说做过的哪个微服务项目&#xff0c;使用了哪个注册中心&#xff0c;常见的有 eurek…

图片在div完全显示

效果图&#xff1a; html代码&#xff1a; <div class"container" style" display: flex;width: 550px;height: 180px;"><div class"box" style" color: red; background-color:blue; width: 50%;"></div><div …

30m二级分类土地利用数据Arcgis预处理及获取

本篇以武汉市为例&#xff0c;主要介绍将土地利用数据转换成武汉市内各区土地利用详情的过程以及分区统计每个区内各地类面积情况&#xff0c;后面还有制作过程中遇到的面积制表后数据过小的解决方法以及一些相关的知识点&#xff1a; 示例数据下载链接&#xff1a;数据下载链…

2024年阿里云服务器新用户购买一个月多少钱?

阿里云服务器一个月多少钱&#xff1f;最便宜5元1个月。阿里云轻量应用服务器2核2G3M配置61元一年&#xff0c;折合5元一个月&#xff0c;2核4G服务器30元3个月&#xff0c;2核2G3M带宽服务器99元12个月&#xff0c;轻量应用服务器2核4G4M带宽165元12个月&#xff0c;4核16G服务…

UnicodeDecodeError: ‘gbk‘和Error: Command ‘pip install ‘pycocotools>=2.0

今天重新弄YOLOv5的时候发现不能用了&#xff0c;刚开始给我报这个错误 subprocess.CalledProcessError: Command ‘pip install ‘pycocotools&#xff1e;2.0‘‘ returned non-zero exit statu 说这个包安装不了 根据他的指令pip install ‘pycocotools&#xff1e;2.0这个根…

哥德巴赫猜想

七十年代末八十年代初&#xff0c;哥德巴赫猜想在中国风靡一时&#xff0c;来源于徐迟的一篇同名报告文学。我还是小孩子&#xff0c;记得大人们叽里咕噜疯传。 “哇&#xff0c;不得了。陈景润证明了1&#xff0b;2&#xff1d;3&#xff0c;离1&#xff0b;1&#xff1d;2就…

misc30

rar解压得到 发现只有中间的图片可以分析&#xff0c;另外两个都有密码 那就先分析星空&#xff0c;属性里面发现 使用该密码可以解压doc文本&#xff0c;发现doc隐写 使用此密码&#xff08;Hello friend!)解压图片,得到一个二维码 扫码得到flag flag{welcome_to_ctfshow}

【Web】浅聊Java反序列化之Rome——关于其他利用链

目录 前言 JdbcRowSetImpl利用链 BasicDataSource利用链 Hashtable利用链 BadAttributeValueExpException利用链 HotSwappableTargetSource利用链 前文&#xff1a;【Web】浅聊Java反序列化之Rome——EqualsBean&ObjectBean-CSDN博客 前言 Rome中ToStringBean的利用…

(001)UV 的使用以及导出

文章目录 UV窗口导出模型的主要事项导出时材质的兼容问题unity贴图导出导出FBX附录 UV窗口 1.uv主要的工作区域&#xff1a; 2.在做 uv 和贴图之前&#xff0c;最好先应用下物体的缩放、旋转。 导出模型的主要事项 1.将原点设置到物体模型的底部&#xff1a; 2.应用修改器的…

线程和进程

参考链接&#xff1a; 1.基本概念 进程&#xff1a;Windows系统中&#xff0c;一个运行的xx.exe就是一个进程。例如打开浏览器就是一个进程 线程&#xff1a;进程中的一个执行任务&#xff08;控制单元&#xff09;&#xff0c;负责当前进程中程序的执行。一个进程至少有一个…

Android14 Handle机制

Handle是进程内部, 线程之间的通信机制. handle主要接受子线程发送的数据, 并用此数据配合主线程更新UI handle可以分发Message对象和Runnable对象到主线程中, 每个handle实例, 都会绑定到创建他的线程中, 它有两个作用,: (1) 安排消息在某个主线程中某个地方执行 (2) 安排…

Python学习之基础语法

一、HelloWorld 二、Python基础语法 2.1 字面量 定义&#xff1a;在代码中&#xff0c;被写下来的固定的值&#xff0c;称之为字面量。 常用的6种值的类型 字符串 Python中&#xff0c;字符串需要用双引号包围&#xff1b; 被双引号包围的都是字符串 666 13.14 "黑马…

深度学习预备知识(线性代数)

介绍&#xff1a; 深度学习是一种机器学习的方法&#xff0c;涉及到大量的线性代数运算。线性代数是研究向量空间和线性映射的数学学科。在深度学习中&#xff0c;线性代数常用于表示和处理输入数据和模型参数。下面是一些深度学习中常见的线性代数概念和运算&#xff1a; 1. …

数据结构之单链表及其实现!

目录 ​编辑 1. 顺序表的问题及思考 2.链表的概念结构和分类 2.1 概念及结构 2.2 分类 3. 单链表的实现 3.1 新节点的创建 3.2 打印单链表 3.3 头插 3.4 头删 3.5 尾插 3.6 尾删 3.7 查找元素X 3.8 在pos位置修改 3.9 在任意位置之前插入 3.10 在任意位置删除…

【python量化】基于okex API开发的海龟策略

介绍 基于okex api开发的海龟策略&#xff0c;okex海龟策略python实现方式。该程序目前只支持单品种&#xff0c;比如设置ETH后&#xff0c;只对ETH进行做多做空。该程序运行需要两样东西&#xff1a;apikey 和 标的 运行该程序之前&#xff0c;用户需要到okex网站去申请apiK…

嘉绩咨询:八位一体产业创新,赋能品牌新零售

探索新零售领域不断创新高峰的嘉绩咨询在今天全面展现了其“八位一体”产业创新模式&#xff0c;该模式旨在为新零售品牌提供全方位的赋能服务。立足于广州的企业战略导航专家&#xff0c;吹响了帮助中国品牌实现全球化发展的号角。 嘉绩咨询的核心业务涵盖招商教育、招商落地、…

Java学习笔记之IDEA的安装与下载以及相关配置

1 IDEA概述 ​IDEA全称IntelliJ IDEA&#xff0c;是用于Java语言开发的集成环境&#xff0c;它是业界公认的目前用于Java程序开发最好的工具。 集成环境&#xff1a; ​把代码编写&#xff0c;编译&#xff0c;执行&#xff0c;调试等多种功能综合到一起的开发工具。 2 IDEA…

使用TTL直接对esp32-cam进行烧录

首先你要有一个usb转TTL下载器和一个esp32-cam 然后我们要将IO0与GND短接 UOR->TXD UOT->RXD 3V3->3V3 GND->GND

[蜥蜴书Chapter2] -- 创建测试集

目录 一、规则 二、方法 1、seed 2、identifier 1&#xff09;选取一个身份号 2&#xff09;选取一定比例的身份号作为测试集 3&#xff09;身份号的选取&#xff1a; 3、利用scikit-learn&#xff1a; 1) 随机生成&#xff1a; 2&#xff09;注&#xff1a;分类 3&a…