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

目录

一、线程间共享数据

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_lockvoid 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.mzph.cn/news/876.shtml

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

相关文章

如何与ChatGPT愉快地聊天

原文链接&#xff1a;https://mp.weixin.qq.com/s/ui-O4CnT_W51_zqW4krtcQ 人工智能的发展已经走到了一个新的阶段&#xff0c;在这个阶段&#xff0c;人工智能可以像人一样与我们进行深度的文本交互。其中&#xff0c;OpenAI的ChatGPT是一个具有代表性的模型。然而&#xff0…

mac安装Golang开发环境及快速入门

目录 一、Mac brew 安装go环境 1.1 安装步骤 1.2 设置GOPATH 及环境变量 1.3 编写第一个go程序 二、快速入门 2.1 快速入门需求 2.2 go学习&#xff08;自用&#xff09; 2.2.1 go基础程序 2.2.2 变量声明 2.2.3 常量和枚举 2.2.4 函数与多种返回值 2.2.5 init函数…

18.Lucas-Kanade光流及OpenCV中的calcOpticalFlowPyrLK

文章目录 光流法介绍OpenCV中calcOpticalFlowPyrLK函数补充reference 欢迎访问个人网络日志&#x1f339;&#x1f339;知行空间&#x1f339;&#x1f339; 光流法介绍 光流描述了像素在图像中的运动&#xff0c;就像彗星☄划过天空中流动图像。同一个像素&#xff0c;随着时…

手写对象浅比较(React中pureComponent和Component区别)

PureComponent和Component的区别 PureComponent会给类组件默认加一个shouldComponentUpdate这样的周期函数 //PureComponent类似自动加了这段shouldComponentUpdate(nextProps,nextState){let { props, state } this;// props/state:修改之前的属性状态// nextProps/nextState…

047、TiDB特性_TopSQL

TopSQL 之前 之前没有办法找单个TiKV Server的语句。只能查找整个集群的慢语句。 TopSQL之后 指定TiDB及TiKV实例正在执行的SQL语句CPU开销最多的Top 5 SQL每秒请求数、平均延迟等信息 TopSQL 使用 选择需要观察负载的具体TiDB Server或TiKV实例 观察Top 5 类SQL 查看某…

用IDEA写第一个Spring程序 HelloWorld

用IDEA写第一个Spring程序 HelloWorld 环境 Orcal JDK&#xff1a;1.8.0_341 maven&#xff1a;3.9.3 Spring&#xff1a;5.3.10 IDEA&#xff1a;2023.1.2 1. 安装JDK和IDEA 2. 安装maven并配置环境变量、换源 3. IDEA中maven属性配置&#xff0c;主要是版本和settings文件及…

python+selenium进行cnblog的自动化登录测试

Web登录测试是很常见的测试&#xff0c;手动测试大家再熟悉不过了&#xff0c;那如何进行自动化登录测试呢&#xff01;本文就基于pythonselenium结合unittest单元测试框架来进行一次简单但比较完整的cnblog自动化登录测试&#xff0c;可提供点参考&#xff01;下面就包括测试代…

centos7 docker 安装sql server 2019

contos7安装sql server docker最低1.8或更高 卸载旧的docker sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine 装docker依赖包 #安装所需资源包 sudo yum install -…

uniapp实现微信小程序自带的分享功能

定义 share.js 文件 export default {data() {return {// 默认的全局分享内容share: {title: 标题,path: /pages/index/index, // 全局分享的路径imageUrl: , // 全局分享的图片(可本地可网络)}}},// 定义全局分享// 1.发送给朋友onShareAppMessage(res) {return {title: this…

数据结构与算法——希尔排序(引例、希尔增量序列、原始希尔排序、代码、时间复杂度、Hibbard增量序列、Sedgewick增量序列)

目录 引例 希尔增量序列 原始希尔排序 代码&#xff08;C语言&#xff09; 时间复杂度 更多增量序列 Hibbard增量序列 Sedgewick增量序列 希尔排序&#xff08;by Donald Shell&#xff09; 引例 给以下序列进行排序&#xff1a; 先以5的间隔进行插入排序&#xff1a…

设计模式之桥接模式

写在前面 本文看下桥接设计模式。 1&#xff1a;介绍 1.1&#xff1a;什么时候桥接设计模式 当一个业务场景由多个变化维度组成&#xff0c;并且这多个变化的维度到底有多少种情况是不确定&#xff0c;比如现在我们要为瑞幸咖啡写一个系统&#xff0c;很自然的&#xff0c;…

2023.7.16 第五十九次周报

目录 前言 文献阅读:跨多个时空尺度进行预测的时空 LSTM 模型 背景 本文思路 本文解决的问题 方法论 SPATIAL 自动机器学习模型 数据处理 模型性能 代码 用Python编写的LSTM多变量预测模型 总结 前言 This week, I studied an article that uses LSTM to solve p…

Element分页组件自定义样式

样式效果 页面代码 <el-paginationsize-change"handleSizeChange"current-change"handleCurrentChange":current-page"page.page":page-sizes"[10, 20, 30, 40]":page-size"page.size"layout"total, sizes, prev, …

如何用https协议支持小程序

步骤一&#xff1a;下载SSL证书 登录数字证书管理服务控制台。在左侧导航栏&#xff0c;单击SSL 证书。在SSL证书页面&#xff0c;定位到目标证书&#xff0c;在操作列&#xff0c;单击下载。 在服务器类型为Nginx的操作列&#xff0c;单击下载。 解压缩已下载的SSL证书压缩…

使用 jmeter 进行审批类接口并发测试

目录 前言&#xff1a; 背景&#xff1a; 难点&#xff1a; 场景 a&#xff1a; 场景 b&#xff1a; 前言&#xff1a; 使用JMeter进行审批类接口的并发测试是一种有效的方法&#xff0c;可以模拟多个用户同时对接口进行审批操作&#xff0c;以评估系统在高负载情况下的性…

Java+Vue+Uniapp全端WMS仓库管理系统

详情图片为运行截图,功能列表: 1、数据管理:物料数据管理、物料Bom管理、物料组管理、物料分类管理、供应商管理、仓库管理、货位管理、车间管理 2、采购管理:物料标签管理、入库单管理、入库退货管理 3、质检管理:质检单管理(包括单据号、单据类型、创建时间、检验状态…

4. CSS用户界面样式

4.1什么是界面样式 所谓的界面样式,就是更改一些用户操作样式,以便提高更好的用户体验。 ●更改用户的鼠标样式 ●表单轮廓 ●防止表单域拖拽 4.2鼠标样式cursor li {cursor: pointer; }设置或检索在对象上移动的鼠标指针采用何种系统预定义的光标形状。 4.3轮廓线outline…

排序算法第三辑——交换排序

目录 ​编辑 一&#xff0c;交换排序算法的简介 二&#xff0c;冒泡排序 冒泡排序代码&#xff1a;排升序 三&#xff0c;快速排序 1.霍尔大佬写的快速排序 2.挖坑法 3.前后指针法 四&#xff0c;以上代码的缺陷与改正方法 三数取中 三路划分&#xff1a; 五&#…

【电子学会】2023年05月图形化四级 -- 计算圆的面积和周长

计算圆的面积和周长 编写程序计算圆的面积和周长。输入圆的半径&#xff0c;程序计算出圆的面积和周长&#xff0c;圆的面积等于3.14*半径*半径&#xff1b;圆的周长等于2*3.14*半径。 1. 准备工作 &#xff08;1&#xff09;保留舞台中的小猫角色和白色背景&#xff1b; 2…

Python 列表 sort()函数使用详解

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;小白零基础《Python入门到精通》 sort函数使用详解 1、升序降序2、sort()和sorted()的区别3、切片排序4、指定排序…