C++11 thread,mutex,condition_variable,atomic,原子操作CAS,智能指针线程安全,单例模式最简单实现方式

1.thread

在c++11中,c++标准委员会开发出了thread库;接下来我们先来看看这个库的使用吧;

1.1 thread类接口介绍

1.1.1 thread类构造函数

我们thread库中的thread类的构造函数可以通过直接传递回调函数与函数的参数来构造线程:

int sub(int x, int y)
{std::cout << x - y << std::endl;return x - y;
}void add(int x, int y)
{std::cout << x + y << std::endl;
}
void test1()
{int x = 1, y = 2;std::thread t1(add, x, y);std::thread t2(sub, x, y);t1.join();t2.join();
}

现象:

相比较与linux底层的pthread_create,这里C++thread的使用更加方便,我们不需要固定线程函数的返回值,也不需要通过结构体来传递函数的多个参数,只需要通过函数的不定参数列表来传递多个不同的参数即可;

此外,线程函数删除了拷贝构造,防止了线程的拷贝,但是我们可以使用thread()空构造来创建空线程,之后再通过移动构造的方式让线程接收到线程函数: 

void testfun2()
{std::cout << "我是线程函数" <<std::endl;
}void test2()
{std::vector<std::thread> v;for (int i = 0; i < 5; i++){v.push_back(std::thread());}for (auto& it : v){it = std::thread(testfun2);}for (auto& it : v){it.join();}
}

现象:

linux下的pthread-create,是通过存储pthread_t的tid,来达到存储线程的效果,而C++这里的线程想存储起来,可以通过上面创建空线程的方式存储,最后通过移动构造来创建出带有线程函数的线程;

1.1.2 thread的各种接口

其实thread类的接口并不多,底层都是封装了linux下的线程代码(windows环境下自然就是封装windows的),我们接下来直接通过使用来加深理解:

joinable :

void testfun3()
{std::cout << "我是线程函数" << std::endl;
}
void test3()
{std::thread t1(testfun3);Sleep(10);//只是让现象更明显,忽略即可std::cout << t1.joinable() << std::endl;t1.join();std::cout << t1.joinable() << std::endl;
}

现象: 

一个线程接收到线程函数时运行起来,没有被join时joinable返回值才为true;如果一个线程被join或者detach又或者只是利用thread()默认构造没有连接线程函数,joinable返回值返回为false;总之,joinable可以用来判断一个线程可不可以被join或者detach;

 detach:

void testfun4()
{std::cout << "我是线程函数" << std::endl;
}
void test4()
{std::thread t1(testfun4);t1.detach();
}

在主线程不关心子进程的返回值时直接让子线程与主线程分离,主线程可以去做自己的事情,不需要阻塞的等待子线程;

get_id: 

void testfun5()
{std::cout << "在线程函数中获取线程id:"<<std::this_thread::get_id()<< std::endl;
}
void test5()
{std::thread t1(testfun5);std::cout <<"t1的线程id:" << t1.get_id() << std::endl;t1.join();
}

 现象:

可以通过get_id获取C++自己维护的线程id,在线程外直接通过线程对象即可获取,在线程内需要通过this_thread作用域调用域中函数get_id才可以获取到线程id; 

1.2 thread函数的参数

1.2.1 传递给thread的方法

 thread函数的第一个参数一定是一个回调方法,这个方法可以是函数指针,仿函数lambda,还可以是function包装器,使用方式是各不相同的;

void testfun6()
{std::cout << "我是函数指针" << std::endl;
}
class fun6 {
public:void operator()(){std::cout << "我是仿函数" << std::endl;}
};
void test6()
{std::thread t1(testfun6);//函数指针std::thread t2([]()->void {std::cout << "我是lambda方法" << std::endl; });//lambdafun6 f;std::thread t3(f);//仿函数t1.join();t2.join();t3.join();
}

现象: 

成功的传递了三种不同的方法(函数);

 1.2.1 传递的参数为引用时要带上ref()

void testfun7(int x,int y,int & z)
{z = x + y;
}
void test7()
{int x = 1, y = 2, z=0;std::thread t1(testfun7,x,y,z);//std::thread t1(testfun7,x,y,std::ref(z));Sleep(10);//为了防止主线程跑太快,创建线程还没有运行std::cout << z << std::endl;t1.join();
}

当我们写出上面这样的代码时,我们想要让z成为我们希望的值 ;

但是我们编译代码是会出现这样的报错:

当出现这样的报错时,我们就会知道是我们的参数传递出现了问题,那么为什么出现了这样的错误呢?

其实这是和thread对参数传递的处理有关;在C++中thread是被封装成为了一个类的,而这个类的构造函数接收了我们传递的参数,而构造函数会通过参数的类型来推理出模板的类型,从而将参数转化为相应的类型(和我们前面的完美转发有些类型),如果我们想要传递的是一个参数的引用,我们就需要显式的声明这个参数是引用,使得这个参数保持引用的状态一直向下传递;

 将z声明为ref(z)即可

	std::thread t1(testfun7,x,y,std::ref(z));

此时再运行:

在我们的linux线程部分,似乎从来没有出现过这样的问题,这其实是C++类模板的推理所出现的问题;而在linux下的线程是通过指针转递的,所以不会出现这样的推理的问题;

介绍完线程我们接下来介绍一下与线程息息相关的mutex

2.mutex

2.1 mutex对象

mutex头文件中的mutex类是一个无法被拷贝的类,并且他的构造函数是无参的构造函数

下面是它的接口:

这些接口的使用其实和linux下线程的使用是一摸一样的,不相同的只是需要通过mutex类来调用而已;(面向对象与面向过程的区别);其中的native_handle这个接口是开放的一个底层的接口,可以通过其去设置pthread_mutex_t类型指针指向的内容,这样就可以设置锁的某些属性,这个接口我也没用过,这里只是作为介绍,也让我自己有所了解;

此外mutex的头文件中基本的锁对象还有有这些锁对象:

2.2 mutex头文件中的其他对象

2.2.1 recursive_mutex 

这个锁对象一般是用在递归函数中,用来解决递归调用时使用普通锁出现的死锁问题,在递归中,我们一般需要使用recursive_mutex而不是普通的mutex锁;

2.2.2 timed_mutex

这个锁提供了一种尝试获取锁并在一定时间后自动放弃的机制。这对于需要防止死锁或者提高线程响应性的应用场景非常有用

接下来我们在来说一说对基本锁对象封装的锁:

 2.2 对基本锁的封装

锁可以保证线程的安全,但在抛异常出现时,执行流的跳转会有致命的死锁出现,那么为了解决这样的问题,RAII风格的锁就出现了:

2.2.1 lock_guard

C++封装了这样的锁,可以让锁上锁后,在这个栈帧被析构时随着栈帧自动释放锁,lock_guard就是这样的封装;

lock_guard的封装很简单没有其他的接口; 

2.2.2 unique_lock

unique_lock可以说是lock_guard的提升版,不仅可以手动释放锁,还增加了时间锁的功能:

 上面是对锁的介绍,接下来我们介绍一下条件变量condition_variable;

3. condition_variable

这个头文件中有这两个基本类:

其中condition_variable: 

 condition_variable只能与std::unique_lock<std::mutex>结合使用,如果想配合其他类型的锁使用费可以使用condition_variable_any类;

下面我们使用 condition_variable类来实现,两个不同线程分别打印1到10的奇数与偶数:

两个线程分别打印奇偶数
std::mutex mtx;
std::condition_variable cond;
int count = 1;void printOdd()
{while (true){std::unique_lock<std::mutex>lck(mtx);if (count > 10){lck.unlock();break;}while (count % 2 == 0)cond.wait(lck);if(count<=10)std::cout<<std::this_thread::get_id()<<": " << count << std::endl;count++;//lck.unlock();cond.notify_one();}
}void printEven()
{while (true){std::unique_lock<std::mutex>lck(mtx);if (count > 10){if (count % 2 == 1)lck.unlock();break;}while (count % 2 == 1)cond.wait(lck);if (count <= 10)std::cout << std::this_thread::get_id() << ": " << count << std::endl;count++;//lck.unlock();cond.notify_one();}
}int main()
{std::thread t1(printOdd);std::thread t2(printEven);t1.join();t2.join();return 0;
}

现象:

 condition_variable的使用与linux下cond条件变量的使用是一样的,只不过上层语言层对其进行了封装使得其具有了跨平台性;

4.atomic

4.1 atomic的基本使用

线程安全问题出现时,我们不仅可以使用锁来控制线程安全,还可以使用atomic原子操作库中的接口来保护线程安全;

我们用这样一个场景来举例:当我们有100个线程时,每个线程对count计数器加加一万次,如果线程安全,那么最后的结果应该是100*10000=1000000,一百万,我们用下面的代码验证;

int count = 0;void add()
{for (int i = 0; i < 10000; i++){count++;}
}void testAtomic()
{std::vector<std::thread> v;for (int i = 0; i < 100; i++){v.push_back(std::thread());}for (auto& it : v){it = std::thread(add);}for (auto& it : v){it.join();}std::cout << "count :" << count << std::endl;
}

测试结果:

此时,如果按照我们平常的操作,我们会使用锁来保证线程安全,但atomic库的出现,我们可以不使用锁,我们可以这样操作:

std::atomic<int> count = 0;

仅仅只需要将我们的count变量用atomic类来包装,就可以使得count的操作是原子的;

4.2 atomic的底层

那么为什么要有这样的原子库呢?我们的锁不是已经可以解决线程安全的问题了吗,为什么还要加一个这样的atomic库呢?我们就得看看atomic的底层CAS无锁编程了;

我们可以将对atomic变量的操作看成下面这样的操作:

上面的atomic变量++操作中与平常的++操作不相同的地方是使用CAS函数来判断count位置内存中的数据是否发生了改变,如果count位置的数据发生了改变就代表有其他线程在我们进行++操作时对count进行修改,所以我们刚刚进行的++操作是无效的需要重新进行;

在atomic库中CAS检查函数的原型是这个:

template< class T >
bool atomic_compare_exchange_weak( std::atomic* obj,T* expected, T desired );
template< class T >
bool atomic_compare_exchange_weak( volatile std::atomic* obj,T* expected, T desired );

而实际实现我们可以看成这样:

bool compare_and_swap (int *addr, int oldval, int newval)
{if ( *addr != oldval ) {return false;}*addr = newval;return true;
}

 如果还需要更多的了解可以看陈皓大佬的博客网站coolshell:

无锁队列的实现 | 酷 壳 - CoolShell

4.3 atomic的优点

我们看了上面的CAS操作我们可以发现这样的保证线程安全的原子操作没有是锁的,这意味着我们的线程不需要对锁资源进行竞争的获取,性能优越,开销小;避免了锁的上下文切换,适合高并发的场景;但是atomic变量对于复杂的线程场景可能会出现问题,我们还是得使用锁来进行复杂逻辑的编程;

5.智能指针引用计数线程安全

在今天之前我们只会使用加锁的方式来保护引用计数的线程安全,不过今天之后,我们就可以使用atomic库来保护线程安全;

class smartPtr{
public:smartPtr(T* ptr = nullptr):_ptr(ptr), _pcount(new std::atomic<int>(1)){}template<class D>smartPtr(T* ptr,D del):_ptr(ptr), _pcount(new std::atomic<int>(1)),_del(del){}......private:T* _ptr;//int* _pcount;std::atomic<int>* _pcount;};

我们只需要将_pcount引用计数更改为atomic类型的变量的指针即可,在构造时,将_pcount初始化为指向堆区的atomic变量的指针;这里的保护很简单,我们还需要注意智能指针本身是线程安全的,但是智能指针保护的资源并不是线程安全的;

6.单例模式最简单实现方式

//单例模式最简单实现方式(懒汉)
class singleClass {
public:static singleClass& getClass(){static singleClass s;return s;}//删除构造和拷贝构造singleClass(const singleClass&) = delete;singleClass& operator=(const singleClass&) = delete;
private:singleClass(){cout << "singleClass()" << endl;}
};int main()
{singleClass::getClass();singleClass::getClass();singleClass::getClass();singleClass::getClass();singleClass::getClass();return 0;
}

现象: 

在C++11后我们可以这样实现单例模式,因为局部的静态变量只会在第一次初始化,保护了线程安全;但如果是在C++11之前无法保证线程是安全的;

其更本原因是:

C++11后的局部静态变量:

如果多个线程同时尝试初始化同一个局部静态变量,只有一个线程会成功初始化该变量,其他线程会被阻塞,直到初始化完成。

这一机制是通过在内部实现中使用适当的锁或原子操作来确保的。这样可以有效地避免线程间的竞争条件,确保了程序的安全性和一致性。

C++11后对局部静态变量做了处理,防止多线程竞争变量时出现的线程安全问题;

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

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

相关文章

THP4 SOP16 芯片 高速光耦芯片

光电耦合器输入端加电信号使发光源发光&#xff0c;光的强度取决于激励电流的大小&#xff0c;此光照射到封装在一起的受光器上后&#xff0c;因光电效应而产生了光电流&#xff0c;由受光器输出端引出&#xff0c;这样就实现了电一光一电的转换。 由于光耦合器输入输出间互相…

mysql主从复制及故障修复

一、主MySQL数据库的配置 分别在三台主机&#xff08;chen2/10.110、chen3/10.120、chen4/10.130)中安装mysql数据&#xff0c;其中chen2/10.110作为主MySQL服务器&#xff0c;其余两台作为从MySQL服务器。 1、在主机上部署mysql数据库 详细的请看上一篇&#xff1a;mysql数据…

2021年江西省职业院校技能大赛(高职组) “云计算应用”赛项样题

2021年江西省职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项样题 【任务 1】基础运维任务[5 分]【题目 1】基础环境配置【题目 2】镜像挂载【题目 3】Yum 源配置【题目 4】时间同步配置【题目 5】计算节点分区 【任务 2】OpenStack 搭建任务[15 分]【题目…

现今 CSS3 最强二维布局系统 Grid 网格布局

深入学习 CSS3 目前最强大的布局系统 Grid 网格布局 Grid 网格布局的基本认识 Grid 网格布局: Grid 布局是一个基于网格的二位布局系统&#xff0c;是目前 CSS 最强的布局系统&#xff0c;它可以同时对列和行进行处理&#xff08;它将网页划分成一个个网格&#xff0c;可以任…

日本HarmonicDrive哈默纳科减速机SHF系列在半导体中的应用

半导体行业作为现代工业的核心领域之一&#xff0c;其技术的不断进步对于推动全球经济和科技创新起着至关重要的作用。而在半导体制造的复杂过程中&#xff0c;各种先进的设备和技术相互配合。日本 HarmonicDrive 哈默纳科减速机 SHF 系列具有优异的定位精度和旋转精度&#xf…

ES6扩展运算符

1.介绍&#xff1a; ... 扩展运算符能将数组转换为逗号分隔的参数序列&#xff1b; 扩展运算符&#xff08;spread&#xff09;也是三个点&#xff08;...&#xff09;。它好比 rest 参数的逆运算&#xff0c;将一个数组转为用逗号分隔的 参数序列&#xff0c;对数组进…

2024年最值得关注的5款数据可视化工具

在信息爆炸的时代&#xff0c;数据可视化工具扮演着至关重要的角色。它们帮助我们从海量数据中提取有价值的信息&#xff0c;并将这些信息以直观、易于理解的方式展现出来。无论是企业决策者、数据分析师还是普通用户&#xff0c;都能通过数据可视化工具更有效地分析和理解数据…

ESP32移植Openharmony外设篇(1)MQ-2烟雾传感器

外设篇 实验箱介绍 旗舰版实验箱由2部分组成&#xff1a;鸿蒙外设模块&#xff08;支持同时8个工作&#xff09;、鸿蒙平板。 其中&#xff0c;鸿蒙平板默认采用RK3566方案。 OpenHarmony外设模块采用底板传感器拓展板方式&#xff0c;底板默认采用ESP32方案&#xff0c;也…

【Linux】【Jenkins】后端maven项目打包教程-Linux版

本次安装版本&#xff1a;2.4 jenkins详细安装教程1、安装git环境2、安装mavne环境2.1 下载依赖2.2、解压、赋权2.2、配置环境变量2.3、验证安装 3、jenkins-插件下载3.1、进入jenkins-->系统管理3.2、进入系统管理-->插件管理3.3、下载两个插件&#xff08;如果之前下载…

【JVM神秘大门】Java虚拟机原理的保姆式教学,零基础速成精英之路(上篇)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

智慧园区的未来:三维可视化系统建设策略

园区三维可视化系统是指利用先进的数字技术和虚拟现实技术&#xff0c;对园区内部空间、设施、资源等进行实时模拟、展示和管理的系统。该系统可以帮助园区管理者更好地了解园区整体布局、资源分配情况&#xff0c;优化空间规划、设施管理&#xff0c;提高园区运营效率和服务水…

江恩理论(Gann Theory)

威廉江恩 威廉江恩&#xff08;William D.Gann&#xff09;&#xff0c;1878年出生于美国德州&#xff0c;二十世纪最著名的投资家。在股票市场上的骄人成绩无人可比。 江恩理论 江恩理论是一种通过数学、几何学、宗教和天文学的综合运用&#xff0c;来分析和预测市场走势的投…

探索 Python 中的 XML 转换利器:xml2dict

文章目录 **探索 Python 中的 XML 转换利器&#xff1a;xml2dict**一、背景介绍二、xml2dict 是什么&#xff1f;三、如何安装 xml2dict&#xff1f;四、基本用法五、实际应用场景六、常见问题及解决方案七、总结 探索 Python 中的 XML 转换利器&#xff1a;xml2dict 一、背景…

【Unity精品插件】Magica Cloth:Unity布料模拟的新高度

&#x1f4c2; Unity 开发资源汇总 | 插件 | 模型 | 源码 &#x1f493; 欢迎访问 Unity 打怪升级大本营 在Unity游戏开发中&#xff0c;物理模拟是使角色和场景更加生动和真实的重要工具之一。Magica Cloth 是一个专为物理效果设计的插件&#xff0c;它提供了轻量级、高性能的…

当LangGraph遇上Mem0:如何让你的AI Agent具有更智能的记忆与个性化的体验?

AI Agent&#xff08;智能体&#xff09;的记忆&#xff08;Memory&#xff09;被认为是一项必备的基础能力&#xff0c;它用来提取、存储会话中的重要信息并用于后续的检索与使用。可以把记忆简单地分成短期记忆与长期记忆两种&#xff0c;用来“记住”不同类型的信息&#xf…

华为CE交换机telnet登录失败故障的排查方法

检查网络状态 操作步骤 1、执行命令ping xxx.xxx.xxx.xxx(目的IP网段) 根据报文是否丢包确认是否可以访问该设备&#xff0c;网络不通请检查组网及网络地址、静态路由等相关配置。 检查VTY通道是否已占满&#xff1f; 操作步骤 1、在任意视图下&#xff0c;执行命令displ…

【MySQL】清理二进制日志文件 binlog.000XXX 以解决 Ubuntu 系统磁盘空间耗尽的问题

问题描述 在使用Ubuntu系统时&#xff0c;发现磁盘空间异常地被填满。通过使用ncdu工具进行检查&#xff0c;结果显示/var/lib/mysql文件夹占用了高达63GB的存储空间。 进一步查看该目录&#xff0c;发现存在几百个以binlog为前缀的文件。 原因分析 这些以binlog为前缀的文件…

2020年计算机网络408真题解析

第一题&#xff1a; 解析&#xff1a;OSI参考模型网络协议的三要素 网络协议的三要素&#xff1a;语法 &#xff0c;语义&#xff0c;同步&#xff08;时序&#xff09; 语法&#xff1a;定义收发双方所交换信息的格式 语法&#xff1a;定义收发双方所要完成的操作 网页的加载 …

汽车电子笔记之-014:一场FIFO的思考引发将汽车电子DTC相关 - 故障发生前后关键数据记录并回读的功能浅研发

目录 1、概述 2、故障发生前数据记录 2.1、环形数组C语言实现 2.2、FIFO的C语言实现 3、故障发生后数据记录 4、数据存储 4.1、数据进FIFO设计思路 4.2、数据出FIFO设计思路 5、数据回读 1、概述 工作中DTC的冻结帧与扩展数据功能一般用于存储故障发生时刻的一些关键数…

Unity Apple Vision Pro 保姆级开发教程 - Simulator 模拟器使用

教程视频 Apple VisionPro Simulator 模拟器使用教程 Unity Vision Pro 中文课堂教程地址&#xff1a; Unity3D Vision Pro 开发教程【保姆级】 | Unity 中文课堂 ​ VsionOS Simulator 简介 visionOS Simulator 是一个用于开发和测试 visionOS 应用程序的工具。它模拟 Appl…