【C++】多线程编程三(std::mutexstd::mutex、std::lock_guard、std::unique_lock详解)

news2024/11/27 20:22:29

目录

一、线程间共享数据

1.数据共享和条件竞争

2.避免恶性条件竞争

 二、用互斥量来保护共享数据

1. 互斥量机制

2.mutex头文件介绍

三、C++中使用互斥量mutex

1. 互斥量mutex使用

 2.mutex类成员函数

① 构造函数

② lock()

③ unlock()

④ try_lock()

四、使用std::lock_guard

五、使用std::unique_lock

六、接口间的条件竞争

七、死锁问题


一、线程间共享数据

1.数据共享和条件竞争

如果共享数据是只读的,那么所有线程都会获得相同的数据,因为不会涉及对数据的修改。但是,当一个线程或多个线程去修改共享数据时,需要考虑到共享数据的一致性问题

当一个线程对共享数据进行修改的同时有其他线程对该共享数据进行读取或修改操作,可能得到的并不是期望的结果,这是常见的错误:条件竞争

并发中竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。大多数情况下,即使改变执行顺序,也是良心竞争,其结果可以接受。例如:有两个线程同时执行读取任务,只要完成相应读取任务就可以了,谁先谁后这时的竞争是没有影响的。

恶性条件竞争通常发生于完成对多余一个的数据块的修改时。

2.避免恶性条件竞争

对数据结构采用某种保护机制,确保只有进行修改的线程才能看到不变量被破坏时的中间状态。从其他访问线程的角度来看,修改不是已经完成了,就是还没开始。(例如:线程A对变量a=0进行+10操作,线程B读到了A线程中a=10,a已经改变,但是A的线程最后把a重新-10,实际对a没有修改。线程B访问到线程A的变量的中间状态)

②对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分割的变化,也就是保证每个不变量保持稳定的状态,这就是所谓的无锁编程

③使用事务的方式去处理数据结构的更新。所需的一些数据和读取都存储在事务日志中,然后将之前的操作合为一步,再进行提交。当数据结构被另一个线程修改后,或处理已经重启的情况下,提交就会无法进行,这称作为“软件事务内存”(software transactional memory (STM))。(例如:线程A对变量a的前后两次修改,修改合成一步,当成事务提交,线程B访问时就只能看到a最后的修改状态)


 二、用互斥量来保护共享数据

1. 互斥量机制

当访问共享数据前,将数据锁住,在访问结束后,再将数据解锁。线程库需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前的线程对数据解锁后,才能进行访问。这就保证了所有线程都能看到共享数据,并不破坏不变量。

2.mutex头文件介绍

C++ 11中与 mutex 相关的类(包括锁类型)和函数都声明在 <mutex> 头文件中,所以如果你需要使用 std::mutex,就必须包含 <mutex> 头文件。

Mutex 系列类(四种)

  • std::mutex,最基本的 Mutex 类。
  • std::recursive_mutex,递归 Mutex 类。
  • std::time_mutex,定时 Mutex 类。
  • std::recursive_timed_mutex,定时递归 Mutex 类。

Lock 类(两种)

  • std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。
  • std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

其他类型

  • std::once_flag
  • std::adopt_lock_t
  • std::defer_lock_t
  • std::try_to_lock_t

函数

  • std::try_lock,尝试同时对多个互斥量上锁。
  • std::lock,可以同时对多个互斥量上锁。
  • std::call_once,如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。


三、C++中使用互斥量mutex

1. 互斥量mutex使用

C++中通过实例化 std::mutex 创建互斥量实例,通过成员函数 lock()对互斥量上锁,unlock()进行解锁。实践中不推荐直接调用成员函数,调用成员函数意味着,必须在每个函数出口去调用unlock(),也包括异常的情况。

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

class Test
{
private:
	std::mutex tmutex;
public:
	void add(int& num) {
		tmutex.lock();//上锁
		++num;
		cout << num << endl;
		tmutex.unlock();//解锁
	}
};

int main()
{
	int num = 100;
	Test t;
	std::thread thread01(&Test::add,&t, std::ref(num));
	std::thread thread02(&Test::add,&t, std::ref(num));
	thread01.join();
	thread02.join();
}

 2.mutex类成员函数

① 构造函数

 作用:构造一个互斥量对象。该对象处于未锁定状态。

            互斥对象不能被复制/移动(该类型的拷贝构造函数和赋值操作符都被删除)。

② lock()

作用:互斥量上锁。

线程调用该函数会发生下面 3 种情况:

(1)如果互斥锁当前没有被任何线程锁定,则调用线程将其锁定(从此时开始,直到调用其成员unlock,该线程拥有互斥锁)。
(2)如果互斥锁当前被另一个线程锁定,则调用线程的执行将被阻塞,直到被另一个线程解锁(其他未锁定的线程继续执行)。

(3)如果互斥锁当前被调用该函数的同一个线程锁定,则会产生死锁(带有未定义的行为)。

③ unlock()

作用:互斥量解锁,释放互斥量的所有权。

如果其他线程在试图锁定同一个互斥量时被阻塞,其中一个线程将获得该互斥锁的所有权并继续执行。

互斥量的所有上锁和解锁操作都遵循单一的总顺序,对同一对象的上锁操作和解锁操作之间是同步的。

如果互斥锁当前未被调用线程锁定,则会导致未定义的行为。

④ try_lock()

作用:尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。

线程调用该函数也会出现3 种情况:

(1)如果互斥量当前没有被任何线程锁定,则调用线程将其锁定(从此时开始,直到调用其成员unlock,该线程拥有互斥锁)。
(2)如果互斥锁当前被另一个线程锁定,则函数失败并返回false,但不会阻塞(调用线程继续执行)。
(3)如果互斥锁当前被调用该函数的同一个线程锁定,则会产生死锁(带有未定义的行为)。


四、使用std::lock_guard

C++标准库为互斥量提供了一个RAII语法的模板类 std::lock_guard,它通过让互斥对象始终处于锁定状态来管理它的对象。 

在构造时,互斥对象被调用线程锁定,在析构销毁时,互斥对象被解锁。它是特别适用于具有自动持续时间直到其上下文结束的对象。通过这种方式,它保证在抛出异常时正确解锁互斥对象。

但是请注意,lock_guard对象并不以任何方式管理互斥对象的生命周期:互斥对象的持续时间应该至少延长到锁定它的lock_guard被销毁为止。

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

class Test
{
private:
	std::mutex tmutex;
public:
	void add(int& num) {
		lock_guard<std::mutex> guard(tmutex);//构造时上锁
		++num;
		cout << num << endl;
	}//析构时解锁
};

int main()
{
	int num = 100;
	Test t;
	std::thread thread01(&Test::add,&t, std::ref(num));
	std::thread thread02(&Test::add,&t, std::ref(num));
	thread01.join();
	thread02.join();
}

定义lock_guard的时候调用构造函数加锁,大括号结束调用析构函数解锁。  

【注意】在使用互斥量来保护数据时,要注意检查指针和引用。切勿将受保护数据的指针或引用传递到互斥锁作用域之外,无论是函数返回值,还是存储在外部可见内存,亦或是以参数的形式传递到用户提供的函数中去。只要没有成员函数通过返回值或者输出参数的形式,向其调用者返回指向受保护数据的指针或引用,数据就是安全的。

缺陷:在定义lock_guard的地方会调用构造函数加锁,在离开定义域的话lock_guard就会被销毁,调用析构函数解锁。这就产生了一个问题,如果这个定义域范围很大的话,那么锁的粒度就很大,很大程序上会影响效率。

所以为了解决lock_guard锁的粒度过大的原因,unique_lock就出现了。


五、使用std::unique_lock

unique_lock会在这个构造函数加锁,然后可以利用unique.unlock()来解锁,所以当你觉得锁的粒度太多的时候,可以利用这个来中途解锁,而析构的时候会判断当前锁的状态来决定是否解锁,如果当前状态已经是解锁状态了,那么就不会再次解锁,而如果当前状态是加锁状态,就会自动调用unique.unlock()来解锁。而lock_guard在析构的时候一定会解锁,也没有中途解锁的功能。

方便肯定是有代价的,unique_lock内部会维护一个锁的状态,所以在效率上肯定会比lock_guard慢。

unique_lock是管理的互斥对象在锁定和解锁两种状态下都具有唯一的所有权。

在构造(或通过对其移动赋值)时,对象获得一个互斥对象,对其锁定和解锁操作。

这个类保证销毁时的状态为解锁(即使没有显式调用)。因此,它作为具有自动持续时间的对象特别有用,因为它保证在抛出异常时正确解锁互斥对象。

请注意,unique_lock对象并不以任何方式管理互斥对象的生命周期:互斥对象的持续时间至少应该延长到管理它的unique_lock被销毁为止。

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock

void print_block (int n, char c) {
    std::unique_lock<std::mutex> lck (mtx);
    for (int i=0; i<n; ++i) {
        std::cout << c;
    }
    std::cout << '\n';
}

int main ()
{
    std::thread th1 (print_block,50,'*');
    std::thread th2 (print_block,50,'$');

    th1.join();
    th2.join();

    return 0;
}

六、接口间的条件竞争

因为使用了互斥量或其他机制保护了共享数据,就不必再为条件竞争所担忧吗?

并不是,你依旧需要确定数据是否受到了保护。例如:

构建一个类似于std::stack结构的栈,除了构造函数和swap()以外,需要对std::stack提供五个操作:push()一个新元素进栈,pop()一个元素出栈,top()查看栈顶元素,empty()判断栈是否是空栈,size()了解栈中有多少个元素。即使修改了top(),使其返回一个拷贝而非引用,对内部数据使用一个互斥量进行保护,不过这个接口仍存在条件竞争。这个问题不仅存在于基于互斥量实现的接口中,在无锁实现的接口中,条件竞争依旧会产生。这是接口的问题,与其实现方式无关。


七、死锁问题

死锁是指多个进程循环等待彼此占有的资源而无限期的僵持等待下去的局面。(一对线程需要对他们所有的互斥量做一些操作,其中每个线程都有一个互斥量,且等待另一个解锁。这样没有线程能工作,因为他们都在等待对方释放互斥量。这种情况就是死锁,它的最大问题就是由两个或两个以上的互斥量来锁定一个操作。)

死锁产生的四个条件:

  • 互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。
  • 请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。
  • 非抢占:一个线程在释放资源之前,其他的线程无法剥夺占用。
  • 循环等待:发生死锁时,线程进入死循环,永久阻塞。

避免死锁的方法

(1)避免嵌套锁

一个线程已获得一个锁时,再别去获取第二个。因为每个线程只持有一个锁,锁上就不会产生死锁。即使互斥锁造成死锁的最常见原因,也可能会在其他方面受到死锁的困扰(比如:线程间的互相等待)。当你需要获取多个锁,使用一个std::lock来做这件事(对获取锁的操作上锁),避免产生死锁。

(待完善)

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

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

相关文章

本地服务器localhost:3000一直连接不上

1.检查使用端口3000的进程: 在Windows上,运行 netstat -ano | findstr :3000在Mac/Linux上,运行lsof -i :3000 这将列出当前使用端口3000的任何进程。您要终止这些进程以释放该端口。 2.检查防火墙规则: 确保您的防火墙允许连接到localhost:3000。在MacOS和Windows上,通常不…

vue2已有项目迁移vue3踩坑记录

升级部分所需package.json版本&#xff0c;如vue及相关ui 参考vue3项目升级 解决运行报错 configureWebpack: {// webpack pluginsplugins: [// Ignore all locale files of moment.js// new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 修改为new webpack.IgnorePlug…

azkaban

访问地址 https://xxx.xxx.xxx.xx:8443/index启动azkaban cd /data/servers/azkaban/executor ./bin/start-exec.sh cd /data/servers/azkaban/server ./bin/start-web.sh修改访问数据库密码 cd /data/servers/azkaban/executor/confvim azkaban.propertiescd /data/servers…

电脑桌面远程连接?外网远程桌面连接内网服务器穿透设置

自己个人电脑远程桌面连接另台服务器时&#xff0c;就可以使用到远程连接的功能&#xff0c;如在公司网络管理员远程连接ERP服务器管理操作。 远程连接就是在远程连接另外一台计算机。当某台计算机开启了远程桌面连接功能后我们就可以在网络的另一端控制这台计算机了&#xff…

人工智能大语言模型微调技术:SFT 监督微调、LoRA 微调方法、P-tuning v2 微调方法、Freeze 监督微调方法

人工智能大语言模型微调技术&#xff1a;SFT 监督微调、LoRA 微调方法、P-tuning v2 微调方法、Freeze 监督微调方法 1.SFT 监督微调 1.1 SFT 监督微调基本概念 SFT&#xff08;Supervised Fine-Tuning&#xff09;监督微调是指在源数据集上预训练一个神经网络模型&#xff…

ORCA优化器浅析——ORCA core流程

DXL query messages is parsed and transformed to an in-memory logical expression tree that is copied-in to the MemoExploration 触发生成逻辑等价表达式的转换规则。探索阶段将新的群组表达式添加到现有组中&#xff0c;并可能创建新的组。Exploration results in addin…

Java性能优化-测试try-catch放在循环内和外的性能对比与业务区别

场景 Java中使用JMH(Java Microbenchmark Harness 微基准测试框架)进行性能测试和优化&#xff1a; Java中使用JMH(Java Microbenchmark Harness 微基准测试框架)进行性能测试和优化_霸道流氓气质的博客-CSDN博客 使用如上方式测试Java中try-catch放在循环内和循环外是否有性…

NiuBi!简单且匿名的密码喷洒工具

工具介绍 通过 Amazon AWS 直通代理启动密码喷洒/暴力破解&#xff0c;为每次身份验证尝试转移请求的 IP 地址。这会动态创建 FireProx API&#xff0c;以实现更规避的密码喷射。 关注【Hack分享吧】公众号&#xff0c;回复关键字【230525】获取下载链接 小心帐户锁定&#xf…

Java 设计模式——工厂方法模式

目录 1.案例分析2.简单工厂模式2.1.结构2.2.案例实现2.2.1.抽象产品2.2.2.具体产品2.2.3.具体工厂2.2.4.测试 2.3.优缺点2.4.扩展 3.✨工厂方法模式3.1.介绍3.1.结构3.2.案例实现3.2.1.抽象工厂3.2.2.具体工厂3.2.3.测试 3.3.优缺点3.4.应用场景 1.案例分析 【需求】设计一个咖…

python获取职教云信息

⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;努力输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;项目。 目录 1、需求1.1、推荐课程1.2、课程详情 2、思路分析2.1、推荐课程完整代码 2.2、课程详情找到JSON数据…

4.2.tensorRT基础(1)-第一个trt程序,实现模型编译的过程

目录 前言1. hello案例2. 补充知识总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT 基础-第一个 trt 程序&#x…

SlickGrid学习

options&#xff1a; 选项 设置 enableCellNavigation 启用单元格导航&#xff0c;可以点单元格 enableColumnReorder 启动拖拽列 example-colspan.html 跨列实例 AutoTooltips plugin 隐藏列文字时自动显现列标题全文 Checkbox row select column 增加选择列来选择行…

STM32入门学习之core_cm3问题

1.安装了keil之后&#xff0c;新建工程出现几百个关于core_cm3的问题&#xff0c;百思不得其解。后在网上查阅资料后&#xff0c;了解到可能是keil版本的问题&#xff0c;是因为我下载的keill版本太高了&#xff0c;内部不支持ARM5.06的编译器。出现很多关于core_cm3的问题是因…

使用java语言制作一个窗体(弹窗),用来收集用户输入的内容

前言 最近做的一个需求&#xff0c;有个逻辑环节是&#xff1a;需要从本地保存的xml文件中取出一个值&#xff0c;这个值是会变化的。然后项目经理就给我说&#xff0c;你能不能做一个小工具&#xff0c;让用户可以直接通过界面化操作将这个变化的值写入文件&#xff0c;不用再…

rv1126交叉编译

目录 一、解压sdk二、交叉编译出动态库sqlite3交叉编译opencv交叉编译一、解压sdk tar xzvf rv1126_rv1109_linux_sdk_v1.8.0_PureVersion.tar.gz 查看交叉编译工具链 pwd查看绝对路径/home

正斜杠“/” 和反斜杠 “\” 傻傻分不清?

Note, on your keyboard, the location of two different punctuation marks—/, or forward slash, and \, or backward slash (also known as backslash). As you read from left to right, the forward slash falls forward while the backslash falls backward; 引用自 《…

js逆向思路-区分各个瑞数版本vmp/3/4/5/6代

目录 一、如何区分是最新瑞数vmp反爬二、3/4/5/6代/vmp版本的瑞数网站特征举例三、瑞数反爬的解决思路四、推荐相关瑞数文章五、一些心得一、如何区分是最新瑞数vmp反爬 前言:本篇文章不会介绍详细的解决反爬的算法扣代码过程,只是一些经验闲谈,文章的末尾有相关的好的质量的…

油猴脚本-Bilibili剧场模式仿Youtube

对比某个不存在的视频网站&#xff08;YouTube&#xff09;&#xff0c;以及B站的播放模式&#xff0c;普通模式以及网页全屏之间都有一个“中间档”&#xff0c;油管的叫 剧场模式&#xff0c;B站的叫 宽屏模式。 剧场模式 宽屏模式 相比之下&#xff0c;还是更喜欢油管的剧…

10.带你入门matlab频率表、盒图(matlab程序)

1.简述 相关概念介绍 以信号为例&#xff0c;信号在时域下的图形可以显示信号如何随着时间变化&#xff0c;而信号在频域下的图形&#xff08;一般称为频谱&#xff09;可以显示信号分布在哪些频率及其比例。频域的表示法除了有各个频率下的大小外&#xff0c;也会有各个频率的…

《遗留系统现代化》读书笔记(基础篇)

你现在所写的每一行代码&#xff0c;都是未来的遗留系统 为什么要对遗留系统进行现代化&#xff1f; 什么是遗留系统&#xff1f; 判断遗留系统的几个维度&#xff1a;代码、架构、测试、DevOps 以及技术和工具。时间长短并不是衡量遗留系统的标准。代码质量差、架构混乱、没…