从汇编角度解释线程间互斥-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}
}
相关文章:
从汇编角度解释线程间互斥-mutex互斥锁与lock_guard的使用
多线程并发的竞态问题 我们创建三个线程同时进行购票,代码如下 #include<iostream> #include<thread> #include<list> using namespace std; //总票数 int ticketCount100; //售票线程 void sellTicket(int idx) {while(ticketCount>0){cou…...
高程 | 多态性(c++)
文章目录 📚多态📚运算符重载🐇定义🐇规则🐇友元运算符重载函数🐇成员运算符重载函数 📚虚函数📚纯虚函数和抽象类 📚多态 多态:同样的消息被不同类型的对象…...
LV.23 D2 开发环境搭建及平台介绍 学习笔记
一、Keil MDK-ARM简介及安装 Keil MDK,也称MDK-ARM,Realview MDK (Microcontroller Development Kit)等。目前Keil MDK 由三家国内代理商提供技术支持和相关服务。 MDK-ARM软件为基于Cortex-M、Cortex-R4、ARM7、ARM9处理器设备…...
[uniapp生命周期]详细讲解uniapp中那些属于vue生命周期,那些属于uniapp独有的生命周期,以及这中间的区别 相关的内容和api 代码注释
目录 1. Vue.js生命周期函数2.Vue生命周期函数代码beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed$nextTick$forceUpdate$destroy 3. UniApp独有的生命周期函数onLaunchonShowonHideonError 4.总结 在UniApp中,除了Vue.js的生命周…...
【动态规划】【记忆化搜索】【状态压缩】1681. 最小不兼容性
作者推荐 【数位dp】【动态规划】【状态压缩】【推荐】1012. 至少有 1 位重复的数字 本文涉及知识点 动态规划汇总 状态压缩 记忆化搜索 1681. 最小不兼容性 给你一个整数数组 nums 和一个整数 k 。你需要将这个数组划分到 k 个相同大小的子集中,使得同一…...
JVM-类加载器 双亲委派机制
申明:文章内容是本人学习极客时间课程所写,文字和图片基本来源于课程资料,在某些地方会插入一点自己的理解,未用于商业用途,侵删。 什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写&a…...
vue axios 请求后端无法传参问题
vue请求后端无法传参问题 问题描述处理过程总结 问题描述 在学习vue时,使用axios调用后端,发现无法把参数正确传到后端,现象如下: 使用vue发起请求,浏览器上已经有传参,但是后端没接收到对应的用户名密码&…...
打印最小公倍数
打印最小公倍数 题目描述: 输入2个整数m和n,计算m和n的最小公倍数,并打印出结果 测试1: 输入:18 24 输出:72 测试2: 输入:18 6 输出:18解法思路: 最小公倍数是指两个…...
[AIGC] Java 和 Kotlin 的区别
好的,我还是以“萌萌哒小码农”的身份继续回答您的问题。 Java 和 Kotlin 是两种不同的编程语言,它们有许多共同点,但也有一些重要的区别。以下是一些常见的 Java 和 Kotlin 的区别: 语法 Kotlin 的语法比 Java 简洁得多&#…...
蓝桥杯电子类单片机提升一——超声波测距
前言 单片机资源数据包_2023 一、超声波测距原理 二、超声波测距的应用 1.超声波的发射 2.单片机知识补充:定时器 3.超声波的接收与计时 4.距离的计算 1)定时器1为16位自动重载+1T11.0592MHz 2)定时器1为16位自动重载&am…...
前端架构: 脚手架开发流程中的难点梳理
脚手架的开发流程 1 )开发流程 创建 npm 项目创建脚手架入口文件,最上方添加: #!/usr/bin/env node 配置 package.json, 添加 bin 属性编写脚手架代码将脚手架发布到 npm 2 )使用流程 安装脚手架 npm install -g your-own-cli …...
django中配置使用websocket
Django 默认情况下并不支持 WebSocket,但你可以通过集成第三方库如 channels 来实现 WebSocket 功能。channels 是一个 Django 应用,它提供了对 WebSocket、HTTP2 和其他协议的支持。 下面是如何在 Django 项目中使用 WebSocket 的基本步骤:…...
Rust复合类型详解
在Rust中,复合类型是一种能够将多个值组合在一起的数据类型。本篇博客将介绍两种常见的复合类型:元组(Tuple)和数组(Array)。 Tuple(元组) 元组是Rust中的一种复合类型,…...
学习 JavaScript 闭包
1. 前言 闭包是 JavaScript 中一种非常重要的概念,它允许函数访问其外部作用域中的变量,即使在函数被返回或者在其原始定义的作用域之外执行时仍然可以访问这些变量。 在讲解闭包之前我们得弄清楚下面的概念: 作用域链: JavaSc…...
VScode中配置 C/C++ 环境 | IT拯救者
文章目录 0 引言1. 下载编辑器VScode2. 下载编译器MinGW并解压3. 将MinGW添加至环境变量4. 配置VScode插件5. 运行代码6. 调整和优化7. 提示8. 例行格式条款9. 例行格式条款 0 引言 由于VScode毛毛张使用不习惯,因此配置教程记不住,不过毛毛张看到一篇不…...
基于Python实现Midjourney集成到(个人/公司)平台中
目前Midjourney没有对外开放Api,想体验他们的服务只能在discord中进入他们的频道进行体验或者把他们的机器人拉入自己创建的服务器中;而且现在免费的也用不了了,想使用就得订阅。本教程使用midjourney-api这个开源项目,搭建Midjou…...
蓝桥杯刷题--python-6
0最大距离 - 蓝桥云课 (lanqiao.cn) n=int(input()) nums=list(map(int,input().split()))max_=float(-inf) for i in range (n):for j in range (i+1,n):tmp=abs(i-j)+abs(nums[i]-nums[j])max_=max(tmp,max_) print(max_) 0最长递增 - 蓝桥云课 (lanqiao.cn) import os im…...
node+vue3+mysql前后分离开发范式——实现对数据库表的增删改查
文章目录 ⭐前言⭐ 功能设计与实现💖 node后端操作数据库实现增删改查💖 vue3前端实现增删改查⭐ 效果⭐ 总结⭐ 结束⭐结束⭐前言 大家好,我是yma16,本文分享关于 node+vue3+mysql前后分离开发范式——实现对数据库表的增删改查。 技术选型 前端:vite+vue3+antd 后端:…...
【Android】使用Apktool反编译Apk文件
文章目录 1. 下载Apktool1.1 Apktool官网下载1.2 百度网盘下载 2. 安装Apktool3. 使用Apktool3.1 配置Java环境3.2 准备Apk文件3.3 反编译Apk文件3.3.1 解包Apk文件3.3.2 修改Apk文件3.3.3 打包Apk文件3.3.4 签名Apk文件 1. 下载Apktool 要使用Apktool,需要准备好 …...
(04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
Hive中的排序通常涉及到order by 、sort by、distribute by 、cluster by 一、语法 selectcolumn1,column2, ... from table [where 条件] [group by column] [order by column] [cluster by column| [distribute by column] [sort by column] [limit [offset,] rows]; …...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...
springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
Fabric V2.5 通用溯源系统——增加图片上传与下载功能
fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...
网站指纹识别
网站指纹识别 网站的最基本组成:服务器(操作系统)、中间件(web容器)、脚本语言、数据厍 为什么要了解这些?举个例子:发现了一个文件读取漏洞,我们需要读/etc/passwd,如…...
初探Service服务发现机制
1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能:服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源…...
Linux 中如何提取压缩文件 ?
Linux 是一种流行的开源操作系统,它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间,使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的,要在 …...
