从汇编角度解释线程间互斥-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.hqwc.cn/news/474460.html

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

相关文章

C++ 多起点的bfs(五十九)【第六篇】

今天我们来学习多起点的bfs 1.多起点的bfs 在普通的广度优先搜索问题中&#xff0c;为了得到从初始状态到达目标状态的最小操作数&#xff0c;则将初始状态放入队列中。离初始状态由近及远地不断扩展出新的状态&#xff0c;直到搜索到目的状态&#xff0c;或队列为空&#xff…

【网络安全】什么样的人适合学?该怎么学?

有很多想要转行网络安全或者选择网络安全专业的人在进行决定之前一定会有的问题&#xff1a; 什么样的人适合学习网络安全&#xff1f;我适不适合学习网络安全&#xff1f; 当然&#xff0c;产生这样的疑惑并不奇怪&#xff0c;毕竟网络安全这个专业在2017年才调整为国家一级…

解锁Spring Boot中的设计模式—03.委派模式:探索【委派模式】的奥秘与应用实践!

委派模式 文章目录 委派模式1.简述**应用场景****优缺点****业务场景示例** 2.类图3.具体实现3.1.自定义注解3.2.定义抽象委派接口3.3.定义具体执行者3.4.定义委派者(统一管理委派任务)3.5.定义委派者管理类 4.测试4.1.controller层4.2.测试不同场景4.2.1.测试生产部门计算费用…

MCU看门狗

目录 一、独立看门狗(IWDG) 1、IWDG 主要作用 2、IWDG 主要特性 3、编程控制 4、注意地方 二、窗口看门狗(WWDG) 1、窗口看门狗作用&#xff1a; 2、窗口看门狗产生复位信号有两个条件&#xff1a; 3、WWDG 框图 4、WWDG 将要复位的时间 5、编程控制 一、独立看门…

跟着pink老师前端入门教程(JavaScript)-day02

三、变量 &#xff08;一&#xff09;变量概述 1、什么是变量 白话&#xff1a;变量就是一个装东西的盒子 通俗&#xff1a;变量是用于存放数据的容器&#xff0c;通过变量名获取数据&#xff0c;甚至数据可以修改 2、变量在内存中的存储 本质&#xff1a;变量是程序在内存…

LeetCode---384周赛

题目列表 3033. 修改矩阵 3034. 匹配模式数组的子数组数目 I 3035. 回文字符串的最大数量 3036. 匹配模式数组的子数组数目 II 一、修改矩阵 简单模拟即可&#xff0c;代码如下 class Solution { public:vector<vector<int>> modifiedMatrix(vector<vecto…

SpringCloud-Nacos集群搭建

本文详细介绍了如何在SpringCloud环境中搭建Nacos集群&#xff0c;为读者提供了一份清晰而详尽的指南。通过逐步演示每个关键步骤&#xff0c;包括安装、配置以及Nginx的负载均衡设置&#xff0c;读者能够轻松理解并操作整个搭建过程。 一、Nacos集群示意图 Nacos&#xff0…

VS中设置#define _CRT_SECURE_NO_WARNINGS的原因和设置方式

原因&#xff1a; 在编译老的用C语言的开源项目的时候&#xff0c;可能因为一些老的.c文件使用了strcpy,scanf等不安全的函数&#xff0c;而报警告和错误&#xff0c;而导致无法编译通过。 解决方案&#xff1a; 我们有两种解决方案&#xff1a; 1、在指定的源文件的开头定…

20-k8s中pod的调度-nodeSelector节点选择器

一、概念 我们先创建一个普通的deploy资源&#xff0c;设置为10个副本 [rootk8s231 dns]# cat deploy.yaml apiVersion: apps/v1 kind: Deployment metadata: name: dm01 spec: replicas: 10 selector: matchLabels: k8s: k8s template: metadata: …

C 练习实例23-打印菱形

题目&#xff1a;打印出如下图案&#xff08;菱形&#xff09;。 * *** ***** ******* ***** *** * 题目分析&#xff1a; 先打印前4行&#xff0c;因为是递增关系。 第0行&#xff1a;打印3个空格&#xff0c;1个* 第1行&#xff1a;打印2个空格&#xff0c;3个*…

(每日持续更新)jdk api之ObjectInputStream.GetField基础、应用、实战

博主18年的互联网软件开发经验&#xff0c;从一名程序员小白逐步成为了一名架构师&#xff0c;我想通过平台将经验分享给大家&#xff0c;因此博主每天会在各个大牛网站点赞量超高的博客等寻找该技术栈的资料结合自己的经验&#xff0c;晚上进行用心精简、整理、总结、定稿&…

云计算基础-华为存储实验

存储配置流程 创建硬盘域&#xff08;CK&#xff0c;包括热备CK&#xff09;创建存储池&#xff08;CKG、Extent&#xff09;创建LUN、LUN组、将LUN加入LUN组创建主机、主机组、将主机加入主机组创建映射视图&#xff08;LUN组和主机组的映射&#xff09; 1. 创建CK 创建硬盘…