【C++11新特性】多线程

news2024/11/15 20:09:07

目录

一、thread类

二、互斥锁

1.四种锁

(1)mutex

(2)recursive_mutex

(3)time_mutex

(4)recurive_timed_mutex

2.lock_guard

3.unique_lock

4.锁的原理

三、原子操作

四、简单的线程池

五、条件变量


一、thread类

在C++11之前,涉及多线程问题,都是和平台相关的,比如windows系统下和linux系统下都有各自的接口,这使得代码的可移植性比较差。C++11最重要的特性就是支持了多线程。使得C++在并行编程时不需要依赖第三方库(可以跨平台使用)。并且在原子操作中引入了原子类的概念,要使用标准库中的线程,必须包含头文件thread。

函数名功能
thread()构造一个线程对象,没有任何关联的线程函数,即没有启动任何线程
thread(fn,args,args2,...)构造一个线程对象,并关联线程函数fn,args1,args2...为线程函数的参数
get_id()获取线程id
joinable()线程是否正在执行,joinable代表一个正在执行的线程
join()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象之后会马上调用,用于把创建线程与线程对象分开,分离的线程变为后台线程,创建的线程的死活与主线程无关

1.线程对象可以关联一个线程,用来控制线程和获取线程的状态。

2.当创建一个线程之后,没有提供任何线程函数,该对象没有对应的任何线程。

下面来写一个简单的创建线程的小程序:

void Func(int n)
{
	cout << n << endl;
}
int main()
{
	thread();//创建一个空线程,什么都不做
	thread t1(Func, 10);
	thread t2([](int n) { cout << n << endl; }, 20);
	t1.join();
	t2.join();
}

有一个细节需要注意,那就是thread在向函数传参的时候,不能使用左值引用进行传参:

void Func(int& n)//用引用接收会发生错误
{
	cout << n << endl;
}
int a=10;
thread t1(Func,a);

这里引入了一个ref函数来满足这一操作。即:

thread t1(Func,ref(a));

二、互斥锁

在C++11中,Mutex包含了四种互斥量的种类:

1.四种锁

(1)mutex

函数名函数功能
lock()上锁
unlock()解锁
try_lock()

非阻塞获取锁

当线程函数调用lock()的时候,有以下三种情况:

(1)锁没有被取走,直接获取锁。

(2)当前线程已经有该锁了,形成死锁。

(3)锁被其他线程取走,进行阻塞等待。

当函数调用try_lock的时候,有以下三种情况:

(1)锁没有被取走,直接获取锁。

(2)该线程已经有该锁了,产生死锁。

(3)锁被其他线程取走,会返回false,而不是被阻塞。

(2)recursive_mutex

允许递归上锁,来过得互斥对象的多层所有权,在释放互斥量的时候也需要采用等量的unlock来进行解锁。

(3)time_mutex

比mutex多了两个成员函数.

函数名函数功能
try_lock_for()接受一个时间范围,表示在一段时间之内,线程如果没有获得锁则被阻塞住,如果此期间其他线程释放了锁,则该线程可以获得锁,如果超时,返回false
try_lock_util()接受一个时间点作为参数,在指定时间未到来之前,线程如果没有获得锁则被阻塞住,如果此期间其他线程释放了锁,则该线程可以获得锁,如果超时返回false

(4)recurive_timed_mutex

使用的比较少,这里不多介绍。

2.lock_guard

lock_guard是C++11中定义的模板类。主要通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁的问题。
缺陷:太单一,用户没有办法对锁进行控制,因此C++11又提供了unique_lock。

3.unique_lock

与lock_gard类似 ,unique_lock类模板也是采用RAII的方式进行封装,并且也是独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。

在构造时,unique_lock对象需要传入一个Mutex对象作为它的参数,新创建的unique_lock对象负责传入的Mutex对象的上锁和解锁的操作。使用以上类型互斥量实例化unique_lock对象的时候,自动调用构造函数上锁,unique_lock更加的灵活,提供了很多成员函数。

上锁、解锁:lock,try_lock,try_lock_for,try_lock_util和unlock

修改操作:移动赋值,交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权),释放(release:返回当前unique_lock所管理的互斥量的指针)

获取属性:owns_lock(返回当前对象是否上来锁),operator>bool(与owns_lock()的功能向同),mutex(返回当前unique_lock所管理的互斥量的指针)

4.锁的原理

我们都知道,锁是来控制信号量的,防止两个线程对信号量进行混乱的修改。

static int x = 0;
void Func1(int n)
{
	for (int i = 0; i < n; i++)
	{
		cout << this_thread::get_id() << "->" << x << endl;
		++x;
	}
}
void Func2(int n)
{
	for (int i = 0; i < n; i++)
	{
		cout << this_thread::get_id() << "->" << x << endl;
		++x;
	}
}
int main()
{
	thread t1(Func1, 10);
	thread t2(Func2, 10);
	t1.join();
	t2.join();
	return 0;
}

在这段代码中,t1和t2两个线程对同一个信号量进行++操作,由于底层的++操作不是原子的,可能会导致x的数值混乱,因为对于一个++操作来说,它底层的汇编大概会分为三步,分别是ld,++,sd

假设当一个线程该执行完读入和x++后,被切走了,第二个线程读入并执行++多次,然后写回。第一个线程时间片再到来的时候,会带着它的上下文数据,发现改进行++了,就进行++操作,然后写回。这就导致线程2做的工作全白做了。因此会造成混乱。

因此需要引入锁这一现象:

当线程1到来时,抢到锁之后会将a1寄存器的值(初值为0)与内存中锁的值进行交换,当时间片结束之后,带着它的上下文数据离开。当线程2到来的时候也会和内存中的mutex的值进行交换,只不过此时mutex的值是0,最终线程2的a1寄存器也为0,因此就可以通过a1寄存器的值来判断谁拿到了锁,从而让它对临界资源进行修改。

那么问题来了,我们应该在循环里面进行枷锁操作还是在循环外面进行加锁操作呢?

如果在循环外枷锁,就相当于两个线程串行运行了,降低了效率。但如果加在里面虽然是并行运行,这样频繁的加锁解锁是需要消耗资源的。

这里我们选择加在循环的外面,因为++执行的太快了,不适合频繁的加锁解锁。

void Func1(int n)
{
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		cout << this_thread::get_id() << "->" << x << endl;
		++x;
	}
	mtx.unlock();
}

三、原子操作

C++将原子操作也封装成了一个对象:

原子类型支持多个原子操作的函数:

可以将上文中的x定义为原子类型,表示的是一条汇编语句就执行了他的++操作。

atomic<int> x = 0;
void Func1(int n)
{
	//mtx.lock();
	for (int i = 0; i < n; i++)
	{
		cout << this_thread::get_id() << "->" << x << endl;
		++x;
	}
	//mtx.unlock();
}

这样书写和加锁的操作是一样的

四、简单的线程池

int main()
{
	atomic<int>x = 0;
	int N, M;
	cin >> N, M;
	vector<thread> vthds;
	vthds.resize(N);
	for (int i = 0; i < N; i++)
	{
		vthds[i] = thread([M,&x]
			{
				for (int i = 0; i < M; i++)
				{
					++x;
				}
			}
		);
	}
	for (auto& e : vthds)
	{
		cout << x << endl;
		e.join();
	}
	return 0;
}

我们可以将vector的每一个元素类型都设为thread类型,在循环调用,循环等待,就可以完成线程池的工作了。

五、条件变量

假如我们设计一个程序,让线程1和线程2交替进行打印,线程1打印奇数,线程2打印偶数。很容易想到使用加锁操作进行解决:

int main()
{
	int n = 100;
	int i = 0;
	mutex mtx;
	thread t1([n, &i, &mtx]
	{
		while (i < n)
		{
			mtx.lock();
			cout << this_thread::get_id() << "->" << i << endl;
			++i;
			mtx.unlock();
		}
	});
	thread t2([n, &i, &mtx]
		{
			while (i < n)
			{
				mtx.lock();
				cout << this_thread::get_id() << "->" << i << endl;
				++i;
				mtx.unlock();
			}
		});
	t1.join();
	t2.join();
	return 0;
}

当运行这段代码的时候很快就能发现问题:我们无法控制两个线程交替枪锁,在大部分的时候都是第一个线程抢到了锁。

这就无法满足我们交替进行打印的条件。这是因为当线程1抢到锁的时候线程2被阻塞住了,那么如何控制两个线程进行交替执行呢?

此时就需要引入条件变量:condition_variable,它提供了几个函数供我们选择:

函数作用
wait阻塞,直到被notify
wait_for最多等待多长时间
notify_one唤醒一个线程
notify_all唤醒所有线程
void wait(unique_lock<mutex>& lck);
void wait(unique_lock<mutex>& lck, Predicate pred);

注意当某个线程调用了wait函数的时候,会调用unlock()释放锁,一旦被notify了,会立刻调用lock()获取锁。因此调用wait的时候需要传入锁。对于第二方式来说第二个参数表示的是一个标记,只有当prep返回值为false的时候才会发生阻塞,相当于

while(!prep())
	wait(lock);

对于notify_one函数来说,当有线程在该条件变量上阻塞的时候,会通知其开始抢锁,当没有线程在条件变量上阻塞的时候,什么都不会做。

我们可以使用条件变量的等待-通知机制来完成两个线程的交替执行:

int n = 100;
int i = 0;
mutex mtx;
condition_variable cv;
bool flag = false;
thread t1([&n, &i, &mtx, &cv, &flag]
	{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag]() {return flag; });
			cout << this_thread::get_id() << "->" << i << endl;
			++i;
			flag = false;
			cv.notify_one();
		}
	}
);
thread t2([&n, &i, &mtx,&cv,&flag]
	{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag]() {return !flag; });
			cout << this_thread::get_id() << "->" << i << endl;
			++i;
			flag = true;
			cv.notify_one();
		}
	});
t1.join();
t2.join();

注意,添加条件变量调用wait的时候,需要将锁进行封装成unique_lock类型,该类型会在创建时调用构造函数自动上锁,在销毁的时候会调用析构函数自动解锁。

分析这段代码,线程1首先获取flag,上锁,wait的第二个变量是false,此时线程1在条件变量下发生阻塞等待;执行到线程2,上锁后,线程2第二个变量是true,继续执行++i,flag设为true,notify_one()唤醒一个线程。

唤醒线程2,阻塞等待,唤醒线程1,继续执行++i,flag设为false,接着唤醒,以此类推。

此时的运行结果是:

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

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

相关文章

供应链采购管理系统:开启智能采购新时代

在当今全球化的商业环境中&#xff0c;供应链管理的高效性和智能化对于企业的生存与发展至关重要。而供应链采购管理系统&#xff0c;尤其是智能采购系统&#xff0c;正成为企业提升竞争力的关键工具。 一、传统采购管理的挑战 传统的供应链采购管理往往面临着诸多难题。首先&a…

【计算机网络 - 基础问题】每日 3 题(二十二)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/fYaBd &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 C 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏&…

【Python】多个dataframe存入excel的不同的sheet表里,而不会被覆盖的方法

我发现&#xff0c;我原来用的多个工作簿存入的方法&#xff0c;发现不太可行了。当使用for循环的时候&#xff0c;原来的sheet 会被新的表给覆盖&#xff0c;后来我找到一种新的方法&#xff1a; with pd.ExcelWriter(file_name ) as writer:Table.to_excel(writer, sheet_na…

程序打开了 但是不显示

程序打开了 但是不显示 失败起因尝试一尝试二 失败 起因 起因是我使用搜狗输入法发现图片表情后台显示打开了&#xff0c;但是在桌面上不显示 尝试一 选中程序&#xff0c;点击alt空格键 会出现还原页面选项 但是我的这个是灰色的 没用 尝试二 https://answers.microsof…

基于单片机的远程无线控制系统设计

摘 要 : 主要介绍了一种以单片机 AT89C2051 、 无线模块 APC200A-43 和继电器为核心的远程智能控制系统。 该系统通过对单片机功能的研究 , 使用单片机的输入输出口和中断实现对控制信号的处理, 通过调试无线通讯模块 , 控制接近开关实现对远程电机的启动 、 停止等控…

窗口函数性能提升50倍,PawSQL索引推荐实战案例

&#x1f31f;引言 在数据驱动的现代世界&#xff0c;SQL查询的速度是应用程序快速响应的关键。尤其是那些涉及窗口函数的复杂查询&#xff0c;若缺乏恰当的索引支持&#xff0c;性能瓶颈可能会成为阻碍。本文将带您看看PawSQL是如何通过智能索引推荐&#xff0c;帮助一个包含…

html+css学习

html 元素 html元素是HTML的根元素&#xff0c;一个文档只能有一个&#xff0c;其他所有元素都是其后代元素 html有一个属性为lang&#xff0c;其作用是&#xff1a; 帮助语言合成工具确定要使用的发音帮助翻译工具确定要使用的翻译规则 当属性lang“en”则表示告诉其浏览器…

数据结构与算法(Python)更新中

一、引入 先来看一道题: 如果 abc1000&#xff0c;且 a^2b^2c^2&#xff08;a,b,c 为自然数&#xff09;&#xff0c;如何求出所有a、b、c可能的组合? 枚举法&#xff1a; # 如果 abc1000&#xff0c;且 a^2b^2c^2&#xff08;a,b,c 为自然数&#xff09;&#xff0c;如何…

XSS闯关小游戏(前13关)

挖掘思路 1.存在可控参数 2.页面存在回显 3.使用带有特殊字符的语句去测试&#xff0c;网站是否进行了实例化 ( 例如 ">123 ) 4.构造闭合&#xff0c;实现payload的逃逸 1 name处参数可控&#xff0c;直接打即可 2 这里知道<>被实体编码了 再测试">1…

想转行AI大模型开发但不知如何下手?看这篇就够了!

原创 最近有很多小伙伴问我&#xff0c;之前从事的其他领域的编程&#xff0c;现在想要学习AI大模型开发的相关技能&#xff0c;不知道从哪下手&#xff0c;应该学习些什么&#xff0c;下面四个是我认为从事大模型开发&#xff0c;必须掌握的四个开源工具&#xff0c;大家可以…

14张图深度解密大厂秒杀系统库存设计,不是所有的库存都能支持高并发!

作者&#xff1a;冰河 星球&#xff1a;http://m6z.cn/6aeFbs 博客&#xff1a;https://binghe.gitcode.host 源码获取地址&#xff1a;https://t.zsxq.com/0dhvFs5oR 沉淀&#xff0c;成长&#xff0c;突破&#xff0c;帮助他人&#xff0c;成就自我。 大家好&#xff0c;我是…

应用在多钥匙应用程序-门锁、遥控器等领域的电容式触摸芯片-GT308L

触摸IC是一种集成了触摸感应技术的芯片&#xff0c;具有快速响应、高精准度和耐用性强的优点。可以实现高灵敏度的触控和准确的指令响应。在智能门锁的设计中&#xff0c;触摸IC不仅可以实现密码输入、指纹识别等功能&#xff0c;还可以与智能家居系统进行联动控制&#xff0c;…

【编程底层原理】Java常用读写锁的使用和原理

一、引言 在Java的并发世界中&#xff0c;合理地管理对共享资源的访问是至关重要的。读写锁&#xff08;ReadWriteLock&#xff09;正是一种能让多个线程同时读取共享资源&#xff0c;而写入资源时需要独占访问的同步工具。本文将带你了解读写锁的使用方法、原理以及它如何提高…

行人3d目标检测-车辆3d目标检测-3d目标检测(代码+教程)

在计算机视觉领域&#xff0c;准确地识别并定位物体对于多种应用来说至关重要&#xff0c;比如自动驾驶、机器人导航以及增强现实等。其中&#xff0c;三维边界框&#xff08;3D Bounding Box&#xff09;估计是一项关键技术&#xff0c;它允许系统不仅能够检测到图像中的物体位…

JavaScript中Windows对象下的属性和方法

1.Windows对象概念 所有的浏览器都支持Windows对象。它表示浏览器窗口 2.Boom概念 Boom&#xff1a;是指浏览器对象模型&#xff0c;它使javaScript有能力与浏览器进行对话 3.DOM概念 DOM&#xff1a;是指文档对象模型&#xff0c;通过它可以访问HTML文档中的所有元素 HT…

达梦-华为鲲鹏ARM架构下性能测试最佳实践

一、测试综述 1.1 测试目的 本次测试的目的是验证达梦数据库&#xff0c;在鲲鹏服务器下&#xff0c;不同服务器参数基于sysbench性能压力测试的表现。本次参数是根据为华为鲲鹏arm服务器调优十板斧内建议值调整 成长地图-鲲鹏开发套件开发文档-鲲鹏社区 1.2 通用指标 指标…

SpringBootWeb响应

2. 响应 前面我们学习过HTTL协议的交互方式&#xff1a;请求响应模式&#xff08;有请求就有响应&#xff09; 那么Controller程序呢&#xff0c;除了接收请求外&#xff0c;还可以进行响应。 2.1 ResponseBody 在我们前面所编写的controller方法中&#xff0c;都已经设置了…

华为云长江鲲鹏深度赋能,大势智慧稳居“实景三维+AI”领域排头兵

本文转自长江日报大武汉客户端 走出象牙塔第10年&#xff0c;武汉大势智慧科技有限公司&#xff08;以下简称“大势智慧”&#xff09;已成长为国内三维技术创新及应用领域龙头企业&#xff0c;其自主研发的“重建大师”等三维测绘软件系统在各级测绘系统占有率达到87.5%。 这…

NLP 文本匹配任务核心梳理

定义 本质上是做了意图的识别 判断两个内容的含义&#xff08;包括相似、矛盾、支持度等&#xff09;侠义 给定一组文本&#xff0c;判断语义是否相似Yi 分值形式给出相似度 广义 给定一组文本&#xff0c;计算某种自定义的关联度Text Entailment 判断文本是否能支持或反驳这个…

Windows系统修改Tomcat虚拟机内存参数

文章目录 I 修改Tomcat虚拟机内存参数基于tomcat管理程序进行配置基于setenv文件进行配置II 查看服务器状态/manager/status 查看服务器状态manager/jmxproxy 查询Tomcat指标I 修改Tomcat虚拟机内存参数 基于tomcat管理程序进行配置 查看堆内存分配情况: jmap -heap pid jst…