C++11之线程库

news2024/12/24 2:55:30

文章目录

  • 一、thread
  • 二、mutex
  • 三、lock_guard 与 unique_lock
    • 1. lock_guard
    • 2. unique_lock
  • 四、atomic
  • 五、condition_variable

在 C++11 之前,涉及到多线程问题,都是和平台相关的,比如 Windows 和 Linux 下各有自己的接口,这使得代码的可移植性比较差。C++11 中最重要的特性就是对线程进行支持了,并且可以跨平台,这使得 C++ 在并行编程时不需要依赖第三方库。C++11 在原子操作中还引入了原子类的概念。

C++11 的线程库在使用上跟 Linux 中的 POSIX 线程库几乎是类似的,不过进行了类的封装。

这篇是关于 Linux 多线程的文章:Linux多线程

在这里插入图片描述

推荐的 C/C++ 参考文档:http://www.cplusplus.com

一、thread

thread 类常用函数:
 ① thread():构造一个线程对象,没有关联任何线程函数,即没有启动任何线程。
 ② thread(fn, args1, args2, ...):构造一个线程对象,并关联线程函数 fn 。args1,args2,… 为线程函数的参数。
 ③ get_id() :获取线程 id 。
 ④ joinable():判断线程对象是否可以被 join 。
 ⑤ join():阻塞等待线程对象终止。
 ⑥ detach():分离线程对象。

注:
 ① 线程函数是可调用对象,可按照以下三种方式提供:函数指针、lambda表达式、函数对象。
 ② thread 类不允许拷贝构造以及赋值重载,但是可以移动构造和移动赋值(将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行)。
 ③ 可以通过joinable()函数判断线程是否是有效的,如果是,则线程无效。
 ④ 采用默认构造函数构造的线程对象。
 ⑤ 线程对象的状态已经转移给其他线程对象。
 ⑥ 线程已经被 join 或者 detach 了。

在某些场景下,我们需要在线程函数内部对外部实参做修改,有两种解决方案。

解决方案1:使用老方法,传指针给线程函数。

#include <iostream>
#include <thread>
using namespace std;

void func(int* p)
{
	cout << "p:" << p << endl;
	(*p) += 10;
}

int main()
{
	int n = 0;
	cout << "&n: " << &n << endl;
	thread t1(func, &n);
	t1.join();

	cout << n << endl;

	return 0;
}

运行结果:n 和 x 的地址相同,n 的值是 10 。


解决方案2:传引用给线程函数,但是需要注意的是,必须使用std::ref()来指明实参是引用类型(否则会报错)。

在一般情况下,传引用传参不需要使用std::ref()来特别指明,但在线程函数中传引用传参时需要,这跟线程函数的底层实现有关。

#include <iostream>
#include <thread>
using namespace std;

void func(int& x)
{
	cout << "&x:" << &x << endl;
	x += 10;
}

int main()
{
	int n = 0;
	cout << "&n: " << &n << endl;
	thread t1(func, std::ref(n));
	t1.join();

	cout << n << endl;

	return 0;
}

运行结果:n 和 x 的地址相同,n 的值是 10 。

二、mutex

在 C++11 中,mutex 总共包含四种互斥锁。但我们最常用的还是 mutex 。

mutex 类常用函数:
 ① lock():加互斥锁(若线程申请锁成功,继续向后执行;若申请锁不成功,会被挂起等待)。
 ② unlock():解互斥锁。
 ③ try_lock():尝试加互斥锁,若互斥锁被其他线程占有,则当前线程立即返回(即相比于lock()try_lock()是非阻塞的)。

我们给临界区加互斥锁,一般情况下,临界区的范围越小越好。

但下面的测试代码是一个极端场景:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int x = 0;  //两个线程对同一个变量x++
mutex mtx;

int main()
{
	thread t1(Func, 50000000);
	thread t2(Func, 50000000);

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

	cout << x << endl;

	return 0;
}
//把加锁和解锁放在循环的外面
void Func(int n)
{
	mtx.lock();
	for (int i = 0; i < n; ++i)
	{
		++x;
	}
	mtx.unlock();
}
//把加锁和解锁放在循环的里面
void Func(int n)
{
	
	for (int i = 0; i < n; ++i)
	{
		mtx.lock();
		++x;
		mtx.unlock();
	}
}

在这个例子中,在保证线程安全的前提下,把加锁和解锁放在循环的里面和外面均可。
不过在这里最好放在循环的外面,因为放在循环的里面会带来效率的降低。可以亲自测试一下。

主要是因为循环里面的代码执行得快(++x 很快),而且锁的申请和释放也有消耗。

  • 若把加锁和解锁放在循环的外面,在这种情况下虽然多个线程是串行执行的。
  • 若把加锁和解锁放在循环的里面,在这种情况下虽然多个线程是并行执行的,但是由于频繁地申请锁和释放锁,频繁地切换上下文,会带来很大的消耗,于是带来效率的降低,反而还不如多线程串行执行。如果把加锁和解锁放在循环的里面,相比于互斥锁,使用自旋锁更好。

三、lock_guard 与 unique_lock

下面的代码在极端情况下会出现死锁问题:

//这是多线程场景,func是所有线程的执行函数
void func(vector<int>& v, int n, int base, mutex& mtx)
{
	try
	{
		for (int i = 0; i < n; ++i)
		{
			mtx.lock();
		
			v.push_back(base + i);
		
			mtx.unlock();
		}
	}
	catch(const exception& e)
	{
		cout << e.what() << endl;
	}
}

push_back()在插入数据时有可能因为空间不够而需要扩容,就向系统申请空间,申请空间失败会抛异常。代码在push_back()处抛异常,不会执行unlock(),线程还未释放锁,进而导致死锁问题,这是一种异常安全问题。

下面来模拟push_back()失败抛异常的情况:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
using namespace std;


void func(vector<int>& v, int n, int base, mutex& mtx)
{
	try 
	{
		for (int i = 0; i < n; ++i)
		{
			mtx.lock();
			
			cout << this_thread::get_id() << ":" << base + i << endl;

			v.push_back(base + i);
			if (base == 1000 && i == 888)  //模拟push_back失败抛异常
			{
				throw bad_alloc();
			}

			mtx.unlock();
		}
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}

int main()
{
	thread t1, t2, t3;
	vector<int> vec;
	mutex mtx;

	try
	{
		t1 = thread(func, std::ref(vec), 1000, 1000, std::ref(mtx));
		t2 = thread(func, std::ref(vec), 1000, 2000, std::ref(mtx));
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	
	t1.join();
	t2.join();

	for (auto e : vec)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

那么如何解决这种情况呢?
可以在 catch 块内部先解锁,因为只有抛异常才会到 catch 块。

catch (const exception& e)
{
	mtx.unlock();  //先解锁
	cout << e.what() << endl;
}

但如果在 try 块中存在多处可能抛异常的地方,就很难知道是谁抛的异常,于是问题就很难处理。
比如下面的代码,存在多处可能抛异常的地方:

//这是多线程场景,func是所有线程的执行函数
void func(vector<int>& v, int n, int base, mutex& mtx)
{
	try
	{
		for (int i = 0; i < n; ++i)
		{
			mtx.lock();
	
			cout << this_thread::get_id() << ":" << base + i << endl;
	
			int* p1 = new int;		//可能抛异常
			v.push_back(base + i);  //可能抛异常
			int* p2 = new int;		//可能抛异常
	
			delete p1;
			delete p2;
	
			mtx.unlock();
		}
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}

实际上,指针变量会交给智能指针来管理,而锁会交给 lock_guard 或 unique_lock 来管理,创建对象来对资源进行管理,对象出了作用域就会调用析构函数来释放资源,于是我们就不需要在 catch 块中考虑诸如解锁和释放内存等问题了。

C++11 采用 RAII 的方式对锁进行了封装,即 lock_guard 与 unique_lock 。

1. lock_guard

lock_guard 的成员函数只有构造函数和析构函数。

lock_guard 在创建对象时,会在构造函数中调用lock()
在销毁对象时,会在析构函数中调用unlock()

下面是对应的代码:

//这是多线程场景,func是所有线程的执行函数
void func(vector<int>& v, int n, int base, mutex& mtx)
{
	try 
	{
		for (int i = 0; i < n; ++i)
		{
			lock_guard<mutex> lock(mtx);  //lock_guard,不会导致死锁

			cout << this_thread::get_id() << ":" << base + i << endl;

			v.push_back(base + i);
			
			if (base == 1000 && i == 888)  //模拟push_back失败抛异常
			{
				throw bad_alloc();
			}
		}
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}

下面是我们简单模拟实现的 lock_guard:

template<class Lock>
class LockGuard    //我们简单模拟实现的lock_guard
{
public:
	LockGuard(Lock& lock)  //引用类型
		:_lock(lock)
	{
		_lock.lock();
	}


	~LockGuard()
	{
		_lock.unlock();
	}

private:
	Lock& _lock;  //引用类型
};

2. unique_lock

由于 lock_guard 比较单一,没有办法对锁进行控制,因此 C++11 又提供了 unique_lock 。

unique_lock 跟 lock_guard 类似,可以用 unique_lock 替换掉 lock_guard 。
但相比于 lock_guard ,unique_lock 更加的灵活,提供了更多的成员函数。

四、atomic

在 C++11 中,如果想要在多线程环境下保证某个共享变量的安全性,除了加互斥锁以外,我们还可以把共享变量的类型定义为原子类型。

由于对原子类型变量的操作本身就是原子的,所以是线程安全的。

测试代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
using namespace std;

atomic<int> x = 0;

void Func(int n)
{
	for (int i = 0; i < n; ++i)
	{
		++x;  //原子操作的++
	}
}

int main()
{
	thread t1(Func, 50000000);
	thread t2(Func, 50000000);

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

	cout << x << endl;

	return 0;
}

五、condition_variable

实现一个程序,使得两个线程交替打印,一个打印奇数,一个打印偶数。

只使用互斥锁是不满足题目要求的。
比如下面的代码:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;


int main()
{
	int n = 100;
	int i = 0;
	mutex mtx;

	//偶数 - 先打印
	thread t1([n, &i, &mtx]{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);

			cout << this_thread::get_id() << ":" << i << endl;
			++i;
		}
	});
	
	//交替走

	//奇数 - 后打印
	thread t2([n, &i, &mtx]{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);

			cout << this_thread::get_id() << ":" << i << endl;
			++i;
		}
	});

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

	return 0;
}

虽然运行结果看上去满足了题目要求,但是也不能排除其中一个线程的时间片比较充足,连续打印了好几次。换言之,上面的程序不能保证一定能满足题目要求。

为了保证一定能够满足题目要求,两个线程交替打印时必须具备一定的顺序性,即线程同步。因此,其中一种方法,就是使用条件变量

condition_variable 类常用函数:

在这里插入图片描述

 ① wait(lck):在条件变量下等待(调用时,会首先自动释放互斥锁,然后再挂起自己;被唤醒时,会自动竞争锁,获取到锁之后才能返回)。
 ② wait(lck, pred):pred 是可调用对象,相当于 while (!pred()) wait(lck);
 ③ notify_one():唤醒在条件变量下等待的一个线程。
 ④ notify_all():唤醒在条件变量下等待的所有线程。

通过使用条件变量,能够实现一个一定能够满足题目要求的程序。
比如下面的代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;


int main()
{
	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);

			//若flag为true,会wait,直到flag为false
			cv.wait(lock, [&flag]()->bool {return !flag; });

			cout << this_thread::get_id() << "->:" << i << endl;
			++i;
			//保证下一个打印的一定是另一个线程,也可以防止该线程连续打印
			flag = true;

			cv.notify_one();
		}
	});
	
	//奇数 - 后打印
	thread t2([n, &i, &mtx, &cv, &flag]{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);

			//若flag为false,会wait,直到flag为true
			cv.wait(lock, [&flag]()->bool {return flag; });

			cout << this_thread::get_id() << ":->" << i << endl;
			++i;
			//保证下一个打印的一定是另一个线程,也可以防止该线程连续打印
			flag = false;

			cv.notify_one();
		}
	});

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

	return 0;
}

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

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

相关文章

PHP另类判断 - 数组是一维还是二维

之前有一个需求&#xff0c;需要判断一个数组是一维还是二维数组&#xff0c;如果是二维的话就要使用foreach循环来处理 在网上搜了一下给出来的都是下面所写的方式&#xff1a; if(count($updata) count($updata,1)) {// 一维 } else {// 二维 }首先我要说的是&#xff0c;上…

第三十七章 数论——博弈论(1)

第三十七章 数论——博弈论&#xff08;1&#xff09;一、Nim游戏1、题目2、结论3、结论验证4、代码二、集合——Nim游戏1、问题2、思路—SG()函数2、代码实现&#xff08;记忆化搜索&#xff09;一、Nim游戏 1、题目 2、结论 这里直接说结论&#xff1a; 假设有nnn堆石子&am…

【LeetCode每日一题】——275.H 指数 II

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 二分查找 二【题目难度】 中等 三【题目编号】 275.H 指数 II 四【题目描述】 给你一个整数数…

Jmeter分布式测试

因为jmeter本身的性能问题&#xff0c;有时候为了尽量模拟业务场景&#xff0c;需要模拟大量的并发请求&#xff0c;此时单台压力机就显得力不从心。针对这个情况&#xff0c;jmeter的解决方案是支持分布式压测&#xff0c;即将大量的模拟并发分配给多台压力机&#xff0c;来满…

三优两重政策解读

什么是三优两重&#xff1a; 优秀大数据产品、优秀大数据解决方案、优秀大数据应用案例和重点大数据企业、重点大数据资源&#xff1b; 1、申报主体 在山东省内注册登记&#xff0c;具备独立承担民事责任的能力&#xff0c;包括各类政府机关、企事业单位及社会组织。 ①.大数据…

【从零开始学习深度学习】33.语言模型的计算方式及循环神经网络RNN简介

目录1. 语言模型1.1 语言模型的计算1.2 nnn元语法的定义2. 循环神经网络RNN2.1 不含隐藏状态的神经网络2.2 含隐藏状态的循环神经网络2.3 应用&#xff1a;基于字符级循环神经网络的语言模型3. 总结1. 语言模型 语言模型&#xff08;language model&#xff09;是自然语言处理…

多媒体服务器核心实现(流管理)

多媒体服务器比较多&#xff0c;实现的功能也很复杂&#xff0c;但其核心就是是转协议&#xff0c;流管理&#xff0c;连接管理&#xff0c;就是一个时序状态机和信令结合的系统。现在的生态有很多现成的轮子&#xff0c;c/c go实现的均可以拿来就用&#xff0c;只需要按一定的…

插槽,依赖注入,动态组件,异步组件,内置组件

插槽&#xff1a;父组件和子组件内容的一个通信 子组件使用<slot>接收父组件传入的内容 如果内容有多个标签时&#xff0c;使用<template>包裹 默认插槽&#xff1a; <template v-slot:default><h2>标题</h2><p>插槽内容</p> <…

Windows——编写jar启动脚本和关闭脚本

文章目录前言启动脚本编写关闭脚本restart.bat 重启脚本前言 假设项目打包后&#xff0c;项目结构为&#xff1a; 此时如果需要再windows环境中进行项目的启动或关闭&#xff0c;需要频繁的手敲命令&#xff0c;很不方便。此时可以编写.bat脚本文件进行项目的控制。 启动脚本…

就业信息追踪|基于Springboot+Vue开发实现就业信息追踪系统

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 收藏点赞不迷路 关注作者有好处 文末获取源…

双向链表,添加,删除一个节点

文章目录前言一、创建双向链表&#xff08;重命名&#xff09;二、添加一个节点1.添加头指针&#xff1a;2.若 头指针为空3.若头指针非空三、删除一个节点1.找到某节点2.将节点从链表中删除四. 展示所有的节点五. 实验效果总结前言 链表有几种&#xff0c;大致分为&#xff1a…

小程序之会议OA项目--其他界面

目录一、tabs组件及会议管理布局1、tabs.js2、tabs.wxml3、tabs.wxss4、app.wxss5、list.js6、list.json7、list.wxml二、个人中心布局1、ucenter/index/index.js2、ucenter/index/index.wxml3、ucenter/index/index.wxss一、tabs组件及会议管理布局 1、tabs.js // component…

UDS - 15.2 RequestDownload (34) service

15.2 请求下载(34)服务 来自&#xff1a;ISO 14229-1-2020.pdf 15.2.1 服务描述 客户机使用requestDownload服务发起从客户机到服务器的数据传输(下载)。 在服务器接收到requestDownload请求消息之后&#xff0c;服务器应该在发送积极响应消息之前采取所有必要的操作来接收数据…

常用图像像素格式 NV12、NV2、I420、YV12、YUYV

文章目录目的RGBYUVYCrCb采样格式YUV 4:4:4 采样YUV 4:2:2 采样YUV 4:2:0 采样YUV 存储格式YUV422&#xff1a;YUYV、YVYU、UYVY、VYUYYUV420&#xff1a;I420、YV12、NV12,、NV21扩展目的 了解常用图像像素格式 RGB 和 YUV,像素格式描述了像素数据存储所用的格式&#xff0c;…

Spring MVC框架学习

前言:本篇博客将从三个方面来写我们要学习SpringMVC的什么: 连接:当用户在游览器中输入一个url之后,能将这个url请求映射到自己写的程序,也就是访问一个地址时,能够连接到门自己写的服务器. 获取参数:用户访问时如果带一些参数,我该怎样获取.返回数据:执行业务代码之后…

NVM实现一台电脑对node的多版本管理。

一、NVM&#xff1a;Node Version Management&#xff1b; 下载地址&#xff1a;Releases coreybutler/nvm-windows GitHubA node.js version management utility for Windows. Ironically written in Go. - Releases coreybutler/nvm-windowshttps://github.com/coreybutl…

JavaScript寒假系统学习之数组(一)

JavaScript寒假系统学习之数组&#xff08;一&#xff09;一、数组1.1 什么是数组1.2 数组创建的2种方式1.2.1 利用new创建数组1.2.2 利用数组字面量创建数组1.3 访问数组元素1.4 遍历数组1.5 数组实战训练1.5.1 计算数组的和以及平均值1.5.2 求数组中的最大值1.5.3 数组转化为…

使用Qemu在Windows上模拟arm平台并安装debian10 arm系统(cd镜像) 安装记录

参考&#xff1a;使用Qemu在Windows上模拟arm平台并安装国产化操作系统_viyon_blog的博客-CSDN博客_qemu windows 镜像&#xff1a;debian-10.12.0-arm64-xfce-CD-1.iso 环境&#xff1a;qemu虚拟机&#xff0c;宿主机win10,amd64 QEMU_EFI.fd: (298条消息) qemu虚拟机的bi…

N皇后问题-leetcode51-java回溯解+详细优化过程

说明&#xff1a;问题描述来源leetcode 一、问题描述&#xff1a; 51. N 皇后 难度困难1592 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之…

实验八、直接耦合多级放大电路的调试

一、题目 两级直接耦合放大电路的调试。 二、仿真电路 图1(a)所示电路为两级直接耦合放大电路&#xff0c;第一级为双端输入、单端输出差分放大电路&#xff0c;第二级为共射放大电路。 由于在分立元件中很难找到在任何温度下均具有完全相同特性的两只晶体管&#xff0c;因而…