C++学习进阶:C++11线程库

目录

前言:

1.线程库的使用

1.1.thread库

1.2.mutex库 

1.3.condition_variable库

1.4.atomic库

2.线程安全问题

2.1.智能指针

2.2.STL容器


前言:

操作系统:线程-CSDN博客 我们曾经在这篇博客中提及了“语言”和“pthread库”之间的关系,也知道了:不同的语言实现多线程的本质就是对不同系统实现的多线程的实现进行封装

// std::thread;

// C++跨平台封装对应系统的thread的实现 ---
#ifdef _WIN32
CreatThread()
#else 
pthread_create();
#endif

线程的学习我们已经在前面的几篇操作系统博客中进行了系统的学习了,接下来我们主要对C++11线程库提供的接口进行学习。

在开始这篇博客的学习之前,我们需要回顾 操作系统:线程互斥|线程同步|锁的概念-CSDN博客

1.线程库的使用

1.1.thread库

在Thread库中,C++11线程库实现了一个thread类

下面是class thread提供的常用接口: 

operator=Move-assign thread支持移动赋值
get_idGet thread id查看当前线程id
joinJoin thread等待当前线程
detachDetach thread分离该线程
joinableCheck if joinable检查是否处于等待
swapSwap threads交换两个线程

 


thread库的使用

 如图为:thread的构造函数,thread支持无参构造和传入可调用对象,与其参数进行初始化。具体用法如代码所示:

void* PrintFunc(const int& num, const string& s)
{
	cout <<"当前线程id为: " << this_thread::get_id() << endl;
	for (size_t i = 0; i < num; i++)
	{
		cout << s.c_str() << endl;
	}
	return nullptr;
}
struct PrintStruct
{
	void operator()(const int& num, const string& s)
	{
		cout << "当前线程id为: " << this_thread::get_id() << endl;
		for (size_t i = 0; i < num; i++)
		{
			cout << s.c_str() << endl;
		}
	}
};
auto PrintLambda = [](const int& num, const string& s) 
{
	cout << "当前线程id为: " << this_thread::get_id() << endl;
	for (size_t i = 0; i < num; i++)
	{
		cout << s.c_str() << endl;
	}
};
function<void(const int& num, const string& s)> func1 = *PrintFunc;

// thread的初步使用
int main()
{
	// 会出现线程安全问题!4个线程同时访问显示器这一块公共资源
	
    // 带参构造,创建可执行线程
	thread t1(PrintFunc, 3, "通过函数指针打印");
	thread t2(PrintStruct(), 3, "通过仿函数打印");
	thread t3(PrintLambda, 3, "通过lambda打印");
	thread t4(func1,3, "通过函数指针打印");

	cout << "thread-1: " << t1.get_id() << endl;
	cout << "thread-2: " << t2.get_id() << endl;
	cout << "thread-3: " << t3.get_id() << endl;
	cout << "thread-4: " << t4.get_id() << endl;

	// 一定需要join不然会崩溃
	t1.join();
	t2.join();
	t3.join();
	t4.join();
}

 c++11中thread库,通过万能引用可以支持左值的可调用对象和右值的可调用对象进入,并且函数指针、仿函数、lambda表达式、function包装器都可以作为参数传入!

void* PrintFunc(const int& num, const string& s)
{
	cout <<"当前线程id为: " << this_thread::get_id() << endl;
	for (size_t i = 0; i < num; i++)
	{
		cout << s.c_str() << endl;
	}
	return nullptr;
}
int main()
{
	int num = 10;
	// 创建10个未初始化的线程
	vector<thread> threads(num);
	
	size_t i = 0;
	// 创造空对象,接着移动赋值
	for (auto& e : threads)
	{
		// 移动赋值---匿名对象为将亡值
		e = thread(PrintFunc, 3, "thread-" + to_string(i++));
	}

	// 主线程进行join
	for (auto& e : threads)
	{
		e.join();
	}

	// // 创造空对象,接着移动构造
	thread t1(PrintFunc, 3, "通过函数指针打印");
	thread t2(move(t1));

	// 移动构造后 不能再对t1做任何操作
	// cout << t1.get_id() << endl;
	// t1.join();

	cout << t2.get_id() << endl;
	t2.join();
}

另外,我们可以通过STL容器进行无参构造多个线程,通过这个thread的容器来进行对线程进行相同的操作!并且我们知道,线程是具有唯一性的,所以不能进行拷贝构造和赋值,但是thread库支持移动赋值和移动构造,将一个线程转交给另一个线程,而不是拷贝两个一样的线程! 


另外在thread库中实现了,this_thread这个命名空间,通过这个命名空间,我们可以访问当前线程的信息,也可以对其进行休眠或者是完成任务后将时间片交给其他线程。

1.2.mutex库 

我们知道多线程访问临界区时会出现数据错误或者数据不一致的问题,所以C++11也根据不同的操作系统统一封装了mutex库。在mutex库中,我们主要讲解mutex、lock_guard、unique_lock。

mutex

mutex _mutex;
{
    _mutex.lock();    // 我们也可以通过_mutex.try_lock(); 如果没加锁就锁上
    // 临界区访问
    _mutex.unlock();
}

具体应用:

// thread + lambda + 锁
int main()
{
	size_t num1 = 0;
	size_t num2 = 0;
	cin >> num1 >> num2;
	size_t result = 0;
	// 锁
	mutex _mutex;

	// 利用捕获列表来传参
	thread t1([num1, &result, &_mutex]()
		{
			for (size_t i = 0; i < num1; i++)
			{
				_mutex.lock();
				result++;
				_mutex.unlock();
			}
		});

	thread t2([num2, &result, &_mutex]()
		{
			for (size_t i = 0; i < num2; i++)
			{
				_mutex.lock();
				result++;
				_mutex.unlock();
			}
		});
}

这里也体现了:在简单的线程逻辑中,我们一般是通过lambda表达式来实现的!


lock_guard和unique_lock

// 守卫锁
int main()
{
	
	mutex _mutex;
	{
		_mutex.lock();
		// todo
		_mutex.unlock();
	}
	// 当在todo区、unlock之前如果抛异常?
	// 就会出现死锁的问题
	// 所以我们需要一个守卫锁---LockGuard---Linux我们手搓过了
	// 来实现在程序异常后,锁资源的正常释放!

	// mutex中实现了lock_guard
	{
		lock_guard<mutex> guard(_mutex);
	}

	// unique_lock
	// 1.支持手动加锁、解锁,配合某些特定场景
	{
		unique_lock<mutex> u_guard(_mutex);
		u_guard.unlock();
		// todo
		u_guard.lock();
	}
}

lock_guard和unique_guard这两个类实现了构造时加锁,析构时解锁。并且unique_lock能够实现在临界区中手动加锁、解锁。

1.3.condition_variable库

condition_variable实例化出对象后,调用wait函数需要和unique_lock配合使用。并且我们可以通过notify_one唤醒一个处于wait阻塞的线程,或者是通过notify_all唤醒所以wait的线程。

// 条件变量
int main()
{
	int num = 1;
	condition_variable signal;
	mutex _mutex;

	// 实现逻辑:奇数、偶数依次打印
	bool flag = false;
	thread t1([&]()
		{
			for (int i = num; i < 10; i++)
			{
				unique_lock<mutex> lock(_mutex);	// 当前生命周期加锁
				while (flag == true)				// 轮询,等待另一个设置为false
				{
					// 阻塞打印奇数,当前线程 
					signal.wait(lock);		// wait时释放锁,被唤醒后加锁
				}
				cout <<"奇数" << this_thread::get_id() << ", " << num << endl;
				num++;
				flag = true;
				// 唤醒打印偶数
				signal.notify_one();
			}

		});

	thread t2([&]()
		{

			for (int i = num; i < 10; i++)
			{
				unique_lock<mutex> lock(_mutex);
				while (flag == false)
				{
					// 唤醒打印偶数,阻塞打印奇数
					signal.wait(lock);
				}
				cout << "偶数" << this_thread::get_id() << ", " << num << endl;
				num++;
				// 偶数打印完计为false,下次循环将奇数释放
				flag = false;
				signal.notify_one();
			}
		});


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

代码实现的逻辑如下:

  1. 在创建线程之前,我们设置此时的flag为false,当创建两个线程之后,此时CPU调度t1、t2的顺序我们是未知的,但是通过flag我们一定确定奇数先打印、然后偶数阻塞直至flag = true
  2. 当奇数完成打印时,将flag置为true,并且notify_one唤醒wait中的偶数线程,因为t2中while条件为假,不进入while则开始打印偶数。与此同时因为flag为true,在t1中会不断的轮询将t1的线程进行阻塞
  3. 1、2即为实现的逻辑,另外我们也要知道奇数拿到锁后进入wait阻塞时,需要将锁释放,不然会导致偶数无法获得锁。

另外当我们分析时,需要考虑多个场景:比如假设t1先被调用,t2后被调用或者是t2先被调用,t1后被调用,以及不同的时间片下分析可能会出现不一样的情况,但可以肯定的是,最终都可以实现两个线程交替打印奇偶数……

1.4.atomic库

对于atomic这个结构体的学习,本质上就是学习对变量进行原子操作。具体的实现称为CAS无锁编程,我们在下面挂了网址,无锁编程的原理大家可以学习一下。

// 较短临界区操作---atomic原子操作
int main()
{
	atomic<int> num = 0;
	// int num = 0;
	thread t1([&num]()
		{
			for (int i = 0; i < 1000; i++)
			{
				++num;
			}

		});
	thread t2([&num]()
		{
			for (int i = 0; i < 1000; i++)
			{
				++num;
			}

		});

	t1.join();
	t2.join();
	std::cout << num << endl;
	// 延申CAS无锁编程
	// https://coolshell.cn/articles/8239.html#google_vignette

	// atomic的实质借助do while循环和bool CAS函数的实现
	
}

2.线程安全问题

2.1.智能指针

对于 unique_ptr,,由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。而对于 shared_ptr,,多个对象需要共用一个引用计数变量,,所以会存在线程安全问题.。但是标准库实现的时候考虑到了这个问题。 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。

我们知道我们在实现shared_ptr时需要维护一个int*的指针变量,当在多线程下,我们对这个智能指针对象进行指向时,这个int*的变量就成为了共享资源,也就是会出现数据不一致的问题。具体处理,就是将int*变量通过atomic<int>*来进行替换,这样就实现“原子性”。

2.2.STL容器

原因是: STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全. 

那么我们具体如何保证STL在多线程编程下的线程安全呢?

  1. 尽量不修改容器的大小和结构。例如我们在生产、消费模型中,我们设计的模型就是一个固定大小的容器,这样就不涉及了STL的修改,自然就没有线程安全了。
  2. 再进行对STL容器访问时,通过锁、条件变量等机制,来限定只允许一个线程来对STL容器进行访问
  3. 使用线程安全的包装器。 

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

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

相关文章

Flutter 引入webview_windows插件,在已经使用$PATH 中的 nuget.exe情况下,windows端构建失败

报错 PS F:\xx\xxxx> flutter run -d windows Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source! Launching lib\main.dart on Windows in debug mode... E:\Some software\Visual Studio\VS 2022\MSBuild\M…

JavaScript 进阶征途:解锁Function奥秘,深掘Object方法精髓

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;JavaScript 精粹 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 &#x1f235;Function方法 与 函数式编程&#x1f49d;1 call &#x1f49d…

Linux|进程控制

进程创建 fork函数初识 在linux中fork函数时非常重要的函数&#xff0c;它从已存在进程中创建一个新进程。新进程为子进程&#xff0c;而原进程为父进程。 返回值&#xff1a;子进程中返回0&#xff0c;父进程返回子进程id&#xff0c;出错返回-1 进程调用fork&#xff0c;当…

ICode国际青少年编程竞赛- Python-2级训练场-数独

ICode国际青少年编程竞赛- Python-2级训练场-数独 1、 Spaceship.step(3)2、 Spaceship.step(3)3、 Spaceship.step(1) Spaceship.turnLeft() Spaceship.step(1)4、 Spaceship.step(3) Spaceship.turnRight() Spaceship.step(1)5、 Spaceship.step(4) for i in range(3):Spaces…

企业级通用业务 Header 处理方案

目录 01: 处理 PC 端基础架构 02: 通用组件&#xff1a;search 搜索框能力分析 03: 通用组件&#xff1a;search 搜索框样式处理 04: 通用组件&#xff1a;Button 按钮能力分析 05: 通用组件&#xff1a;Button 按钮功能实现 06: 通用组件&#xff1a;完善 search 基本…

MySQL学习笔记11——数据备份 范式 ER模型

数据备份 & 范式 & ER模型 一、数据备份1、如何进行数据备份&#xff08;1&#xff09;备份数据库中的表&#xff08;2&#xff09;备份数据库&#xff08;3&#xff09;备份整个数据库服务器 2、如何进行数据恢复3、如何导出和导入表里的数据&#xff08;1&#xff09…

ARP命令

按照缺省设置&#xff0c;ARP高速缓存中的项目是动态的&#xff0c;每当发送以恶个指定的数据报且高速缓存中不存在当前项目时&#xff0c;ARP便会自动添加该项目。一旦高速缓存的项目被输入&#xff0c;就已经开始走向失效状态。因此&#xff0c;如果ARP高速缓存中的项目很少或…

擎天科技与禅道合作,打造统一的项目管理平台

统一、全面的项目管理平台能够帮助企业优化管理流程&#xff0c;提升业务效率。擎天集团选择与禅道软件合作&#xff0c;打造统一的项目管理平台&#xff0c;在降低自研软件的研发成本、打破团队信息孤岛、保障数据全面性等方面效果显著&#xff0c;大大提高了团队沟通协作效率…

如何使用 ArcGIS Pro 计算容积率

容积率是指地上建筑物的总面积与用地面积的比率&#xff0c;数值越小越舒适&#xff0c;这里为大家介绍一下如何使用ArcGIS Pro 计算容积率&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的建筑和小区数据&#xff0c;除了建筑和小区数据&am…

verilog中不重叠序列检测

编写一个序列检测模块&#xff0c;检测输入信号&#xff08;a&#xff09;是否满足011100序列&#xff0c; 要求以每六个输入为一组&#xff0c;不检测重复序列&#xff0c;例如第一位数据不符合&#xff0c;则不考虑后五位。一直到第七位数据即下一组信号的第一位开始检测。当…

AngularJS基本概念

版本&#xff1a; AngularJs 1.x&#xff1a;https://angularjs.org/ AngularJs 2&#xff1a;https://angular.io/ 或 https://angular.cn/ 实现语言&#xff1a; Angular 1.x&#xff1a;使用ES(avaScript)编写&#xff0c;可直接在浏览器中运行。 Angular 2&#xff1a…

Electron-Vue 脚手架避坑实录,兼容Win11,升级electron22,清理控制台错误

去年的还是有用的&#xff0c;大家继续看&#xff0c;今年再补充一些Electron-Vue 异常处理方案 M1 和 Window10_electron异常处理-CSDN博客 代码gitee.com地址 electron-demo: electron 22 初始代码开发和讲解 升级electron为22版本&#xff08;这个版本承上启下&#xff0c…

内网穿透速度慢

内网穿透速度慢原因及优化策略 在计算机网络应用中&#xff0c;内网穿透是一个常见的需求&#xff0c;它允许外部网络访问位于内部网络&#xff08;如企业局域网或家庭网络&#xff09;中的设备或服务。然而&#xff0c;有时用户在进行内网穿透时会遇到速度慢的问题&#xff0…

【二次元MMORPG游戏开发】任务系统技术拆解

引言 各位同学大家好。在今天的分享当中&#xff0c;我将对任务系统去做一个拆解。也许你见过很多任务系统&#xff0c;但是今天我要分享的是我们经过一个框架迭代以后的任务系统。我会结合客户端的功能演示给大家去讲解。 跟着演示学开发 基本操作 好&#xff0c;首先我们点…

C++ | Leetcode C++题解之第80题删除有序数组中的重复项II

题目&#xff1a; 题解&#xff1a; class Solution { public:int removeDuplicates(vector<int>& nums) {int n nums.size();if (n < 2) {return n;}int slow 2, fast 2;while (fast < n) {if (nums[slow - 2] ! nums[fast]) {nums[slow] nums[fast];slo…

【Linux】项目自动化构建工具make/makefile

&#x1f389;博主首页&#xff1a; 有趣的中国人 &#x1f389;专栏首页&#xff1a; Linux &#x1f389;其它专栏&#xff1a; C初阶 | C进阶 | 初阶数据结构 小伙伴们大家好&#xff0c;本片文章将会讲解Linux中项目自动化构建工具make/makefile的相关内容。 如果看到最后…

[windows系统安装/重装系统][step-2]BIOS设置UEFI引导、磁盘分区GPT分区、安装系统[含完整操作拍照图片]

背景 先准备U盘启动盘和系统镜像: [windows系统安装/重装系统][step-1]U盘启动盘制作&#xff0c;微软官方纯净系统镜像下载 前言&#xff08;略长&#xff0c;建议可跳过&#xff09; 我的笔记本升级了CPU升级了内存后出现了一个小问题&#xff0c; 每次启动徽标显示后会…

hyper-v启动centos7虚拟机不能联网

虚拟网卡要和之前虚拟机里面设置的GATEWAY一致。 安装CentOS遇到Error setting up base repository换url 或者换镜像包iso(这个有用&#xff09; 没掌握摸Yu的精髓 好累啊

安全加固

目录 1.文件锁定管理 2.设置用户账户有效期 3.查看并清除命令历史记录 4.设置用户超时登出时间 5.用户切换 6.用户提权 7.禁用重启热键CtrlAltDel 8.设置单用户模式密码 9.调整BIOS引导设置 10.禁止root用户从本地登录&#xff1a; 11.禁止root用户通过ss…

【算法刨析】完全背包

完全背包与01背包的区别 01背包对于一个物品只能选择一次&#xff0c;但是完全背包可以选择任意次&#xff1b; 思路 和01背包类似&#xff0c;01背包我们只需要判断选或不选&#xff0c;完全背包也是如此&#xff0c;不同的是&#xff0c;对于这个物品我们在判断选后在增加一…