从汇编角度解释线程间互斥-mutex互斥锁与lock_guard的使用

多线程并发的竞态问题

我们创建三个线程同时进行购票,代码如下 

#include<iostream>
#include<thread>
#include<list>
using namespace std;
//总票数
int ticketCount=100;
//售票线程
void sellTicket(int idx)
{while(ticketCount>0){cout<<ticketCount<<endl;ticketCount--;std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms}
}int main()
{list<std::thread> tlist;//存储线程for(int i=1;i<=3;i++)//创建三个线程{tlist.push_back(std::thread(sellTicket,i));}for(auto& tl:tlist){tl.join();//让主线程等待子线程执行结束}return 0;
}

我们再看这段代码的汇编过程 

ticketCount--;

汇编代码如下:

mov eax,ticketCount
sub eax,1
mov ticketCount,eax

上述汇编过程的解读为:

  • 将ticketCount的值从内存放到寄存器eax
  • 通过寄存器完成减法操作
  • 将运算结果再从eax寄存器中放到内存中

可以看到,三个线程在执行代码时,每个线程在执行到ticketCount--时,在底层都会执行上述三行汇编代码,这种竞态必然会导致最终结果的错误。

如:

  • 假如现在ticketCount的值为100
  • 线程一把ticketCount的值从内存放到寄存器并完成了减法操作,则此时ticketCount的值为99,但并未将计算后的结果放到内存,也就是说此时内存中ticketCount的值仍旧为100
  • 线程二开始执行代码,那么线程二从内存取出ticketCount的值放到eax寄存器时必然为100,因此线程二在进行计算后的结果也是99
  • 之后线程一又开始继续执行代码,将他的计算结果99写回内存,则此时输出结果为99
  • 切换到线程二继续执行代码,然而线程二的结果也是99

可以看到,本来两个线程在执行减法操作后,ticketCount的结果应该为98,但是现在的结果却都是99。

出现上述结果的原因就在于ticketCount--代码执行的汇编过程不是一次性完成的

mutex互斥锁

这就是互斥锁出现的作用——保证ticketCount--代码的汇编过程一次性执行

  • std::mutex 是 C++11 引入的互斥量(Mutex)类,用于在多线程环境中实现互斥访问共享资源。

  • 通过 std::mutex,可以确保在同一时间只有一个线程可以访问被保护的临界区,从而避免多个线程同时对共享数据进行修改而导致的数据竞争问题。

  • std::mutex 提供了 lock() 和 unlock() 方法,分别用于锁定和解锁互斥量。需要注意的是,在编写多线程程序时,必须确保每次 lock() 操作都会有对应的 unlock() 操作,以避免死锁等问题。

修改后的代码如下:

#include<iostream>
#include<thread>
#include<list>
#include<mutex>
using namespace std;
//总票数
int ticketCount=100;
std::mutex mtx;
//售票线程
void sellTicket(int idx)
{while(ticketCount>0){mtx.lock();//加锁cout<<ticketCount<<endl;ticketCount--;//解锁mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms}
}int main()
{list<std::thread> tlist;//存储线程for(int i=1;i<=3;i++)//创建三个线程{tlist.push_back(std::thread(sellTicket,i));}for(auto& tl:tlist){tl.join();//让主线程等待子线程执行结束}return 0;
}

但是上述代码仍旧有一些问题,考虑以下情况

  • 假如ticketCount的值为1
  • 由于ticketCount大于0,因此线程一进入while循环并获取锁,但并未执行--操作,因此此时ticketCount的仍旧为1
  • 假如此时线程二刚好被切换,那么由于此时ticketCount的值还没有变化,仍旧为1大于0,因此线程二也进入while循环,但是线程一并未释放锁,因此线程将被卡住
  • 之后线程一继续执行,执行减法操作,ticketCount的值为0,并释放锁
  • 此时线程二继续执行,但是线程二已经进入while循环了,因此线程二也将执行一次减法操作,故而就会出现ticketCount=-1的情况

因此修正后的代码应该是

void sellTicket(int idx)
{while(ticketCount>0){mtx.lock();if(ticketCount>0){cout<<ticketCount<<endl;ticketCount--;}mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms}
}

lock_guard

由于mutex需要程序员时刻记住在何时加锁在何时释放锁,否则就会导致死锁问题,但大多数时候这个工作比较繁琐,并且很容易忘记释放锁,因此出现了lock_guard,可自动管理加锁和解锁

  • std::lock_guard 是 C++11 提供的 RAII(资源获取即初始化)风格的锁管理工具,用于自动管理 std::mutex 的加锁和解锁操作。
  • 通过 std::lock_guard,可以在作用域内自动锁定 std::mutex,并在作用域结束时自动释放锁,从而避免忘记手动解锁或异常情况下未能正确解锁互斥量。
  • std::lock_guard 的构造函数接受一个 std::mutex 对象,并在构造时锁定该互斥量,在析构时释放锁。因此,使用 std::lock_guard 可以很方便地实现线程安全的代码块。

 

void sellTicket(int idx)
{while(ticketCount>0){// mtx.lock();{lock_guard<mutex> lock(mtx);if(ticketCount>0){cout<<ticketCount<<endl;ticketCount--;}}// mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms}
}

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

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

相关文章

内网安全-内网穿透

目录 内网渗透 Nc使用详解 Nc监听和探测 Nc传文件 termite内网穿透工具 ssh代理内网穿透 ssh配置socket代理 MSF多级网络穿透 内网渗透 Nc使用详解 Nc监听和探测 Nc传文件 termite内网穿透工具 1、termite 之前叫ew &#xff08;可以进行正向连接&#xff0c;可以…

OpenWrt的ssh无法登陆问题Permission denied, please try again.解决

OpenWrt的ssh无法登陆问题Permission denied, please try again.解决 一、情况描述&#xff1a; ​ 我根据B站的博主提供的教程对红米AC2100路由器进行刷机。最开始刷的Openwrt是可以连接ssh的&#xff0c;后面的升级改进版就无法进入ssh了&#xff0c;具体的错误如下。 #采…

vue双向绑定的原理

Vue双向绑定的原理主要基于数据劫持和发布-订阅模式。通过使用Object.defineProperty方法来劫持数据属性的setter和getter&#xff0c;当数据发生变化时&#xff0c;能够触发相应的监听器。 具体来说&#xff0c;当一个组件的属性值发生变化时&#xff0c;Vue会触发一个更新函…

倒计时56天

复习3-2&#xff1a;习题篇&#xff1a; 3. #include<bits/stdc.h> using namespace std; #define int long long const int N2e56; const int inf 0x3f3f3f3f; int a[1100][1100]; int b[1100][1100][4]; int n,m,q; int dfs(int i,int j,int q) {if(i<0||j<0||…

栈模拟递归,LeetCode 145. 二叉树的后序遍历

一、题目 1、题目描述 给你一棵二叉树的根节点 root &#xff0c;返回其节点值的 后序遍历 。 2、接口描述 ​ /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nul…

JVM学习记录

JVM基础概念 JVM是一个运行在计算机上的程序&#xff0c;负责编译java字节码文件&#xff0c;支持跨平台特性。 java语言为了通过实时解释实现多平台支持&#xff0c;性能相对于C等语言较低&#xff0c;而JVM提供了JIT即时编译进行性能优化。 JVM与JIT JVM负责解释和执行Ja…

3.10 Binance_interface APP U本位合约交易-市单价平仓

Binance_interface APP U本位合约交易-市单价平仓 Github地址PyTed量化交易研究院 量化交易研究群(VX) py_ted目录 Binance_interface APP U本位合约交易-市单价平仓1. APP U本位合约交易-市单价平仓函数总览2. 模型实例化3. 同步 市价平仓4. 同步 市价平仓 回调函数5. 异步…

LLaMA 入门指南

LLaMA 入门指南 LLaMA 入门指南LLaMA的简介LLaMA模型的主要结构Transformer架构多层自注意力层前馈神经网络Layer Normalization和残差连接 LLaMA模型的变体Base版本Large版本Extra-Large版本 LLaMA模型的特点大规模数据训练 LLaMA模型常用数据集介绍公共数据来源已知的数据集案…

子集型回溯和组合型回溯

目录 子集 1,选或不选 2.枚举选哪个 组合 1.选或不选 2.枚举选哪个 回溯问题有两种思考方式,一种是对于给定集合的每个元素,你是选还是不选,另一种是每个位置必须选一个数,你挑一个选就行了.但这种挑选一定是有序的挑 子集 子集 1,选或不选 class Solution { public:…

Java的接口

目录 1.接口的概念 2.语法规则 3.接口的使用 4.接口的特性 总结&#xff1a; 5.实现多个接口 6.接口间的继承 1.接口的概念 接口就是公共的行为规范标准&#xff0c;大家在实现时&#xff0c;只要符合规范标准&#xff0c;就可以通用。 在Java中&#xff0c;接口可以看成…

【for循环——讲解】

for循环 1. 介绍2. Python 中的 for 循环3. JavaScript 中的 for 循环4. Java 中的 for 循环5. C# 中的 for 循环6. C 中的 for 循环 1. 介绍 for 循环是一种常见的控制结构&#xff0c;被用于在编程中重复执行一段代码固定的次数&#xff0c;或者遍历数据结构中的每个元素。大…

C语言学习(8)—— 输入输出

文件在程序中是以流的形式来操作的&#xff1a;&#xff08;1&#xff09;输入流&#xff1a;数据从数据源(文件)到程序(内存)的路径&#xff1b;&#xff08;2&#xff09;输出流&#xff1a;数据从程序(内存)到数据源(文件)的路径 一、屏幕输入输出 1. getchar() 和 putcha…

MOMENTUM: 1

攻击机 192.168.223.128 目标机 192.168.223.146 主机发现 nmap -sP 192.168.223.0/24 端口扫描 nmap -sV -p- -A 192.168.223.146 开启了22 80端口 看一下web界面 随便打开看看 发现这里有个参数id&#xff0c;sql尝试无果&#xff0c;发现写入什么&#xff0c;网页显示…

kali 开启ssh

vi /etc/ssh/sshd_config PermitRootLogin yes PasswordAuthentication yes 保存退出 service ssh restart ss -tunlp Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process tcp LISTEN …

C#,21根火柴棍问题(21 Matchticks Problem)的算法与源代码

一、21根火柴棍问题&#xff08;21 Matchticks Problem&#xff09; 21根火柴棍问题是西方经典游戏之一。 给定21根火柴&#xff0c;2个人A和B&#xff08;比如&#xff1a;分别是计算机和用户&#xff09;。 每个人一次可以挑选 1-- 4 根火柴。 被迫挑最后一根火柴的人输了…

51单片机 发送信息到电脑 com3口

51单片机 发送信息到电脑 com3口 import serial# 打开串行端口 ser serial.Serial(COM3, 9600) # 9600为波特率&#xff0c;根据实际情况进行调整 flag True try:while True:if ser.in_waiting > 0: # 如果串口有数据可读data ser.read() # 读取一行数据并解码为字符串…

第76讲安全退出实现

安全退出实现 VueX 是一个专门为 Vue.js 应用设计的状态管理构架&#xff0c;统一管理和维护各个vue组件的可变化状态(你可以理解成 vue 组件里的某些 data )。 Vuex有五个核心概念&#xff1a; state, getters, mutations, actions, modules。 state&#xff1a;vuex的基本数…

【回溯】37. 解数独

37. 解数独 解题思路 通过solveSudoku方法开始求解数独问题&#xff0c;然后调用backtrack方法进行递归回溯搜索。backtrack方法通过两个参数i和j来确定当前搜索的位置。 在backtrack方法中&#xff0c;首先检查当前位置是否超出了数独的边界&#xff0c;若超出&#xff0c;则…

【Java】内置锁是什么?

内置锁&#xff08;也称为监视器锁或互斥锁&#xff09;是Java语言提供的一种基本的线程同步机制。在Java中&#xff0c;每个对象都自动持有一个内置的锁&#xff0c;这个锁也被称为对象的监视器锁&#xff08;monitor lock&#xff09;。当你使用synchronized关键字来同步一个…

arduino uno R3驱动直流减速电机(蓝牙控制)

此篇博客用于记录使用arduino驱动直流减速电机的过程&#xff0c;仅实现简单的功能&#xff1a;PID调速、蓝牙控制 1、直流减速电机简介2、DRV8833电机驱动模块简介3、HC-05蓝牙模块简介电机转动测试4、PID控制5、蓝牙控制电机 1、直流减速电机简介 我在淘宝购买的电机&#x…