C++——C++11线程库

目录

一,线程库简介

二,线程库简单使用

2.1 传函数指针

​编辑 2.2 传lamdba表达式

2.3 简单综合运用

2.4 线程函数参数

三,线程安全问题

3.1 为什么会有这个问题?

3.2 锁

3.2.1 互斥锁

3.2.2 递归锁

3.3 原子操作

3.3.1 原子操作变量

3.3.2 atomic模板

五,lock_guard和unique_lock

5.1 可能发生的情况

5.2 lock_guard

5.3 unique_lock

六,两个线程交替打印,一个打印奇数,一个打印偶数


一,线程库简介

在Linux中我们可以使用用户层的线程接口来实现线程操作,但是Linux下的接口无法在Windows下使用,因为Linux支持了POSIX线程标准,但是Windows没有支持,它搞了一套属于它自己的线程标准。所以在C++11之前,涉及到多线程问题的代码可移植性比较差

所以C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类得概念。

C++,Linux和Windows下都可以支持多线程程序 -- 条件编译
#ifdef _WIN32
	CreateThread...
#else
    pthread_create...
#endif

下面是C++中常用的线程函数的介绍:

①thread():构造一个线程对象,没有关联任何线程函数,即没有启动任何线程

②thread(fn, args1, args2...):构造一个线程对象,并使其关联函数fn,args是fn线程函数的参数

③get_id:获取线程id

④join():类似malloc或new之后需要手动free或者delete,join被调用后会阻塞主先,当该线程结束后,主线程继续执行

⑤detach():一般在创建线程对象后马上调用,用于把创建出来的线程与线程对象分离开,分离后的线程变为后台线程,在这之后后台线程就与主线程无关

二,线程库简单使用

线程函数一般情况下可以按照三种方式提供:①函数指针  ②lamdba表达式 ③函数对象

2.1 传函数指针

先来个最简单的线程使用,如下代码:

void Func(int n, int num)
{
	for (int i = 0; i < n; i++)
	{
		cout <<num<<":" << i << endl;
	}
	cout << endl;
}

void main()
{
	thread t1(Func, 10, 1);
	thread t2(Func, 20, 2);
	
	t1.join();
	t2.join();
}

注:由于是多线程同时运行,所以打印结果会比较乱,属于正常情况!

 2.2 传lamdba表达式

我们还可以直接用lamdba表达式传给线程,如下代码:

void main()
{
	int n1, n2;
	cin >> n1 >> n2;
	thread t1([n1](int num)
		{
			for (int i = 0; i < n1; i++)
			{
				cout << num << ":" << i << endl;
			}
			cout << endl;
		}, 1);

	thread t2([n1](int num)
		{
			for (int i = 0; i < n1; i++)
			{
				cout << num << ":" << i << endl;
			}
			cout << endl;
		}, 2);

	t1.join();
	t2.join();
}

2.3 简单综合运用

但是单独用lamdba表达式传过去的话,代码会重复,所以也可以用循环把多个线程放进数组里,如下代码:

void main()
{
	int m;
	cin >> m;
	//要求m个线程分别打印n
	vector<thread> vthds(m);//把线程作为一个对象放到容器里去
	size_t n;
	for (size_t i = 0; i<m; i++)
	{
		size_t n = 10;
		vthds[i] = thread([i, n]() {
			for (int j = 0; j < n; j++)
			{
				cout << this_thread::get_id() << ":" << j << endl; //用this_thread命名空间打印线程id
				this_thread::sleep_for(chrono::seconds(1)); //每打印一次休眠一秒
			}
			cout << endl;
			});
		//cout << vthds[i].get_id() << endl;
	}

	for (auto& t : vthds) //库线程禁掉了拷贝赋值,所以这里用引用
	{
		t.join();
	}
	
}

2.4 线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此,即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参

void ThreadFunc1(int& x)
{
 x += 10;
}
void ThreadFunc2(int* x)
{
 *x += 10;
}
int main()
{
 int a = 10;
 // 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
引用的是线程栈中的拷贝
 thread t1(ThreadFunc1, a);
 t1.join();
 cout << a << endl;

 // 如果想要通过形参改变外部实参时,必须借助std::ref()函数
 thread t2(ThreadFunc1, std::ref(a);
 t2.join();
 cout << a << endl;

 // 地址的拷贝
 thread t3(ThreadFunc2, &a);
 t3.join();
 cout << a << endl;

 return 0;
}

三,线程安全问题

3.1 为什么会有这个问题?

先看下面代码:

unsigned long sum = 0;

void fun(size_t num)
{
 for (size_t i = 0; i < num; ++i)
 sum++;
}
int main()
{
 cout << "Before joining,sum = " << sum << std::endl;
 thread t1(fun, 10000000);
 thread t2(fun, 10000000);
 t1.join();
 t2.join();
 cout << "After joining,sum = " << sum << std::endl;
 return 0;
}

 从上面的运行结果可以看出,两个线程都对一个全局变量进行相加时,结果与我们预期的不同。

多线程最主要的问题就是访问临界资源带来的线程安全问题,如果所有临界资源都是只读的,那就是线程安全的,因为数据不会被修改,但是当一个或多个线程要修改临界资源时,如果没有对临界资源给予相关的保护措施,比如++,那么就是线程不安全的,因为我们说一条汇编语句是原子的,但是++等操作经过编译器编译后会编程三条汇编语句,那么++操作就不是原子的,具体的底层细节我们到linux系统编程部分再讲。

3.2 锁

关于锁的概念我们到Linux系统编程再进行讲解,这里只展示用法:

3.2.1 互斥锁

list<int> lt;
int x = 0;
mutex mtx;
void Func2(int n)
{
	//并行
	for (int i = 0; i < n; i++)
	{
		mtx.lock(); 
		++x;
		lt.push_back(x);
		mtx.unlock();

		cout << i << endl;
		cout << i << endl;
		cout << i << endl;
	}

	//串行 --> 如果我们只是++,没有push_back等其他操作时,可以看出并行并不比串行快,并行会有大量的加锁和解锁,而且由于计算太快了,还有切换线程的切换上下文的消耗
	//但是当我们++后再push_back插到链表后,还是并行快,而且随着多临界资源的访问消耗变大时,并行的优势更大
	/*mtx1.lock(); 
	for (int i = 0; i < n; i++)
	{
		++x;
		lt.push_back(x);
		cout << i << endl;
		cout << i << endl;
		cout << i << endl;
	}
	mtx1.unlock();*/
}

void main()
{
	int n = 2000000;
	size_t begin = clock();
	thread t1(Func2, 10000); //每个线程都有自己独自的栈,这个栈在共享区中,由库提供
	thread t2(Func2, 20000);

	t1.join();
	t2.join();

	size_t end = clock();
	cout << (end - begin) << endl;
	cout << x << endl;
}

 上面的有点复杂,我们直接传lamdba

void main()
{
	int n = 20000;
	int x = 0;
	mutex mtx1;
	size_t begin = clock();
	thread t1([&, n](){
		mtx.lock();
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		mtx1.unlock();
	});
	thread t2([&, n]() {
		mtx1.lock();
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		mtx1.unlock();
		});

	t1.join();
	t2.join();

	size_t end = clock();
	cout << (end - begin) << endl;
	cout << x << endl;
}

3.2.2 递归锁

recursive_mutex mtx2;
void Func3(int n)
{
	if (n == 0)
		return;
	mtx2.lock();
	++x;
	Func3(n - 1);
	mtx2.unlock(); //当普通锁的解锁在这里定义的时候会造成死锁,递归锁解决在递归场景中的死锁问题
}
void main()
{
	thread t1(Func3, 1000); 
	thread t2(Func3, 1000);

	t1.join();
	t2.join();
	cout << x << endl;
}

3.3 原子操作

虽然加锁可以解决线程安全问题,但是加锁有一个缺陷就是:只要一个线程在对sum++,其他线程就必须阻塞等待,一旦线程很多的情况下就会影响总体的效率,而且如果控制不好容易造成死锁问题。

因此C++11引入了原子操作,也叫无锁操作,需要用到头文件#include<atomic>

(该操作也是用到了一个叫做CAS的同步原语,就是当我写一个数的时候,如果这个数已经改变过了,那我就不写了,如果每改变就写)

3.3.1 原子操作变量

#include <thread>
#include <atomic>

atomic_long sum{ 0 }; //这只是其中一个原子操作变量

void fun(size_t num)
{
 for (size_t i = 0; i < num; ++i)
 sum ++;   // 原子操作
}
int main()
{
 cout << "Before joining, sum = " << sum << std::endl;
 thread t1(fun, 1000000);
 thread t2(fun, 1000000);
 t1.join();
 t2.join();
 
 cout << "After joining, sum = " << sum << std::endl;
 return 0;
}

3.3.2 atomic模板

atmoic<T>  t; //声明一个类型为T的原子类型变量t

 注意:原子类型通常属于“资源型”数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行改造,不允许原子类型进行拷贝构造,移动构造和operator=等,所以为了防止意外,标准库直接将atomic模板类中的拷贝构造,移动构造,赋值运算符重载全部删除了

#include <atomic>
int main()
{
 atomic<int> a1(0);
 //atomic<int> a2(a1);   // 编译失败
 atomic<int> a2(0);
 //a2 = a1;               // 编译失败
 return 0;
}

下面是atomic模板类的演示代码:

void Func5(int n)
{
	cout << x << endl;
}
void main()
{
	int n = 200;
	//atomic<int> x = 0; 三种写法都一样,都是去调用构造函数
	//atomic<int> x = {0};
	atomic<int> x{ 0 };
	thread t1([&, n]() {
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		});
	thread t2([&, n]() {
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		});
	Func5(x.load());

	t1.join();
	t2.join();
	cout << x << endl;
}

五,lock_guard和unique_lock

5.1 可能发生的情况

在多线程中,如果想要保证临界资源的安全性,可以将其设置为原子类型避免死锁,也可以通过加锁保证一段代码的安全性。但是还有一种情况,那就是如果在加锁和解锁中间抛异常了,那么代码会直接跳到捕获的地方导致无法执行unlock来释放锁。因此,C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock

5.2 lock_guard

template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lk(lk)
	{
		_lk.lock();
	}
	~LockGuard()
	{
		_lk.unlock();
	}
private:
	Lock& _lk; //三类成员必须在初始化列表初始化:引用,const和没有默认构造的成员变量
};
void Func4(int n)
{
	for (int i = 0; i < n; i++)
	{
		try
		{
			//把锁交给对象,构造函数时加锁,析构函数解锁 --> RAII
			//mtx.lock();
			//LockGuard<mutex> lock(mtx); // --> 我们自己实现的
			lock_guard<mutex> lock(mtx);  // --> 库里的
			++x;
			//...抛异常
			if (rand() % 3 == 0)
			{
				throw exception("抛异常");
			}
			mtx.unlock();
		}
		catch (const exception& e)
		{
			cout << e.what() << endl;
		}
	}
}
void main()
{
	thread t1(Func4, 100);
	thread t2(Func4, 100);

	t1.join();
	t2.join();
}

 通过上述代码可以看到,lock_guard类模板通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方生成一个lock_guard,调用构造函数自动上锁,然后出作用域时通过析构函数自动解锁

5.3 unique_lock

lock_guard的缺陷:太单一,用户没办法对其进行控制,因此C++11还提供了unique_lock.

unique_lock与lock_guard类似,也是采用RAII进行封装,但是unique_lock对象需要传一个mutex对象作为参数,对传入的锁进行上锁和解锁操作。而且它还提供了更多的成员函数:

①上锁/解锁操作:lock,try_lock,try_lock_for,try_lock_until和unlock

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

③获取属性:owns_lock(返回当前对象是否上了锁),operator bool(),mutex(返回当前unique_lock所管理互斥量的指针)

六,两个线程交替打印,一个打印奇数,一个打印偶数

//C++也支持条件变量(不是线程安全的,所以要配合锁使用)
//题目:两个线程交替打印,一个线程打印奇数,一个打印偶数
//condition_variable::wait  当前进程会进行阻塞,直到被唤醒,和linux一样,在阻塞之前会进行解锁,防止死锁问题,然后唤醒后重新申请锁
//文档中的 wait_for和wait_until表示等待的时间,notify_one和notify_all表示唤醒一个线程和唤醒所有线程
#include<condition_variable>
void main39()
{
	condition_variable cv;
	int n = 100;
	int x = 1;

	//如何做到t1和t2不管谁抢到锁都保证t1先运行,t2阻塞 --> t1不等待,t2有等待,这样t1获得了锁,t2就等着,t2获得了锁但是wait,释放锁,t1获得锁
	//如何防止一个线程不断申请锁释放锁,不断运行?( t1打印的时候,释放锁并且通知t2,t2重新获得锁,但是t1没停下来又等待了,t1又获得了锁) --> t1, if(x%2 == 0) cv.wait(lock);  t2,if (x % 2 != 0) cv.wait(lock);
	//为什么要防止一个线程连续打印? --> 假设t1,先获取到锁,t2后获取到锁,t2就阻塞在锁上面 --> t1打印奇数,++x,x变成偶数 --> t1 notift唤醒,但是没有线程wait(因为t1有锁,t2没有锁所以在阻塞等待) --> t1解锁,t1时间片到了,切出去了 
	// --> t2获取到锁,打印,notify,但是没有线程等待,t2再出作用域,解锁 --> t2的时间片充裕会比t1先获得锁,所以如果没有条件控制,就会导致t2连续打印
	thread t1([&, n]() {
		while(1)
		{
			unique_lock<mutex> lock(mtx);
			if (x >= 100)
				break;
			//if(x%2 == 0) //奇数
			//	cv.wait(lock);
			cv.wait(lock, [&x]() {return x % 2 != 0; });
			cout << this_thread::get_id() << ":" << x << endl;
			x++;
			cv.notify_one();
		}
	});
	thread t2([&, n]() {
		while(1)
		{
			unique_lock<mutex> lock(mtx);
			if (x > 100)
				break;
			//if (x % 2 != 0) //偶数
			//	cv.wait(lock);
     		cv.wait(lock, [&x](){return x % 2 == 0; });


			cout << this_thread::get_id() << ":" << x << endl;
			++x;
			cv.notify_one();
		}
	});
	t1.join();
	t2.join();
}

unsigned long sum = 0;

void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++;
}
int main()
{
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于springboot实现房屋租赁系统项目【项目源码+论文说明】

基于springboot实现房屋租赁系统演示 摘要 社会的发展和科学技术的进步&#xff0c;互联网技术越来越受欢迎。网络计算机的生活方式逐渐受到广大人民群众的喜爱&#xff0c;也逐渐进入了每个用户的使用。互联网具有便利性&#xff0c;速度快&#xff0c;效率高&#xff0c;成本…

网络类型整理

1、点到点 &#xff1a;在一个网段内只能存在&#xff0c;两个物理节点 MA-多路访问 -- 在一个网段内物理节点的数量不限制 MA--- BMA NBMA 2、BMA -- 广播型多路访问 3、NBMA--非广播型多路访问 注&#xff1a;不同网络类型实际为不同的数据链路层技术&#xff1b;由于二…

AI渣土车监测报警摄像机

随着城市建设的不断发展和交通运输的快速增长&#xff0c;渣土车作为建筑行业中不可或缺的运输工具&#xff0c;承担着大量的渣土运输任务。然而&#xff0c;由于渣土车在运输过程中存在超速、违规变道、碾压行人等交通安全问题&#xff0c;给道路交通和行人安全带来了严重的隐…

OrangePi AIpro(香橙派)远程连接,在windows上显示图形化桌面

OrangePi AIpro&#xff08;香橙派&#xff09;远程连接&#xff0c;在windows上显示图形化桌面 一、连接调试串口1、硬件连接&#xff08;1&#xff09;首先需要准备一根 Micro USB接口&#xff08;老安卓线&#xff09;的数据线&#xff08;2&#xff09;将 Micro USB 接口一…

Docker之ruoyi-vue项目部署

文章目录 创建自定义网络安装redis安装mysql发布若依项目--后端使用Dockerfile自定义镜像运行容器 nginx 创建自定义网络 #搭建net-ry局域网&#xff0c;用于部署若依项目 docker network create net-ry --subnet172.68.0.0/16 --gateway172.68.0.1 注意1&#xff1a;关闭宿主…

【uC/OS-III篇】uC/OS-III 创建第一个任务(For STM32)

uC/OS-III 创建第一个任务&#xff08;For STM32&#xff09; 日期&#xff1a;2024-3-30 23:55&#xff0c;结尾总结了今天学习的一些小收获 本博客对应的项目源码工程 源码项目工程 1. 首先定义错误码变量 // 用于使用uC/OS函数时返回错误码 OS_ERR err; 2. 定义任务控制…

非NVIDIA平台下的CUDA的替代方案OpenCL,第一步如何获取PlatformInfo、DeviceInfo

非NVIDIA平台下的CUDA的替代方案OpenCL&#xff0c;第一步如何获取PlatformInfo、DeviceInfo 介绍 当谈到高性能计算&#xff0c;NVIDIA的CUDA框架无疑是一个强大的工具。OpenC&#xff08;Open Computing Language&#xff09;是一个更为通用的解决方案&#xff0c;或者你使用…

MySQL面试必备一之索引

本文首发于公众号&#xff1a;Hunter后端 原文链接&#xff1a;MySQL面试必备一之索引 在面试过程中&#xff0c;会有一些关于 MySQL 索引相关的问题&#xff0c;以下总结了一些&#xff1a; MySQL 的数据存储使用的是什么索引结构B 树的结构是什么样子什么是复合索引、聚簇索…

SVFormer: Semi-supervised Video Transformer for Action Recognition

标题&#xff1a;SVFormer&#xff1a;用于动作识别的半监督视频Transformer 原文链接&#xff1a;https://doi.org/10.48550/arXiv.2211.13222 源码链接&#xff1a;GitHub - ChenHsing/SVFormer 发表&#xff1a;CVPR 摘要 半监督动作识别是一项具有挑战性但至关重要的任…

2024年道路运输企业安全生产管理人员证模拟考试题库及道路运输企业安全生产管理人员理论考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年道路运输企业安全生产管理人员证模拟考试题库及道路运输企业安全生产管理人员理论考试试题是由安全生产模拟考试一点通提供&#xff0c;道路运输企业安全生产管理人员证模拟考试题库是根据道路运输企业安全生产…

day58 动态规划part15

392. 判断子序列 简单 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串。&#xff08;例如&#xff0c;"ace"是"abcde&q…

通天星CMSV6 车载定位监控平台 任意文件上传漏洞复现(XVE-2023-23454)

0x01 产品简介 通天星CMSV6车载定位监控平台拥有以位置服务、无线3G/4G视频传输、云存储服务为核心的研发团队,专注于为定位、无线视频终端产品提供平台服务,通天星CMSV6产品覆盖车载录像机、单兵录像机、网络监控摄像机、行驶记录仪等产品的视频综合平台。 0x02 漏洞概述 …

汇编语言第四版-王爽第2章 寄存器

二进制左移四位&#xff0c;相当于四进制左移一位。 debug命令实操&#xff0c;win11不能启动&#xff0c;需要配置文件 Windows64位系统进入debug模式_window10系统64位怎么使用debugger-CSDN博客

DeepL Pro3.1 下载地址及安装教程

DeepL Pro是DeepL公司推出的专业翻译服务。DeepL是一家专注于机器翻译和自然语言处理技术的公司&#xff0c;其翻译引擎被认为在质量和准确性方面表现优秀.DeepL Pro提供了一系列高级功能和服务&#xff0c;以满足专业用户的翻译需求。其中包括&#xff1a; 高质量翻译&#xf…

vue3 视频播放功能整体复盘梳理

回顾工作中对视频的处理&#xff0c;让工作中处理的问题的经验固化成成果&#xff0c;不仅仅是完成任务&#xff0c;还能解答任务的知识点。 遇到的问题 1、如何隐藏下载按钮&#xff1f; video 标签中的controlslist属性是可以用来控制播放器上空间的显示&#xff0c;在原来默…

MySQL数据库高阶语句②

目录 一.子查询与多表查询 1.子查询 2.update子查询 3.多表查询 4.delete子查询 5.exists关键字也用于子查询 6.结果集 二.MySQL视图 1.定义 2.作用场景 3.视图与表的区别与联系 &#xff08;1&#xff09;区别 ①视图是已经编译好的sql语句。而表不是 ②视图没有…

unity 打包安卓错误汇集

Failed to find target with hash string "android-34’ in: D:Pr 他说找不到sdk34level的我用as打开后卸载又重装&#xff0c;最后解决了 我放到Plugins/Android/下面的Java代码没有被编译 这个不知道为什么。我故意把代码写的有问题&#xff0c;会报错那种&#xff…

linux自定义命令

文章目录 1、自定义命令介绍2、自定义命令步骤 (centos7)2.1 新建隐藏目录存放自定义命令脚本文件2.2 将新建的目录配置环境变量2.3 取别名的方式简化已有命令2.4 编写自定义命令脚本 1、自定义命令介绍 不管是linux系统还是windows系统都支持自定义命令&#xff0c;windows端…

MIPI CSI-2 Low Level Protocol解读

一、Low Level Protocol介绍 LLP 是一种面向字节的基于数据包的协议&#xff0c;支持使用短数据包和长数据包格式传输任意数据。为简单起见&#xff0c;本节中的所有示例均为单通道配置。 LLP特性&#xff1a; 传输任意数据&#xff08;与有效载荷无关&#xff09; 8 位字大…

代码随想录第二十五天 | 回溯算法P2 | ● 216● 17

216.组合总和III 找出所有相加之和为 n 的 k 个数的组合&#xff0c;且满足下列条件&#xff1a; 只使用数字1到9每个数字 最多使用一次 返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次&#xff0c;组合可以以任何顺序返回。 示例 1: 输入: k 3, n 7 输出…