【C++】线程库

news2025/1/10 17:05:13

文章目录

  • 线程库(thread)
  • 线程安全
  • 实现两个线程交替打印1-100

线程库(thread)

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

image-20230501230952794

  • 常见的接口
成员函数功能
join该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
get_id获取线程id
detach将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待
thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程

注意:get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类

thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。

  • 使用
void func(int n)
{
	for (int i = 0; i <= n; i++)
	{
		cout << i << endl;
	}
}
int main()
{
	thread t1;//thread提供了无参的构造函数
	t1 = thread(func, 10);//thread提供了移动赋值函数
    thread t3 = thread(func, 10);//移动构造函数
	t1.join();
    t3.join();
	return 0;
}

thread的带参的构造函数的定义如下:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
  • fn:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。
  • args...:调用可调用对象fn时所需要的若干参数。

调用带参的构造函数创建线程对象:

void func(int n)
{
	for (int i = 0; i <= n; i++)
	{
		cout << i << endl;
	}
}
int main()
{
	thread t2(func, 10);
	t2.join();
	return 0;
}

利用Lambda表达式创建n个线程对象,每个线程跑m次,同时打印出线程的id,调用thread的成员函数get_id可以获取线程的id,但该方法必须通过线程对象来调用get_id函数,如果要在线程对象关联的线程函数中获取线程id,也就是没有对象的情况下,可以调用this_thread命名空间下的get_id函数:

int main()
{
	int n,m;
	cin >> n>>m;
	vector<thread> v;
	v.resize(n);
	for (auto& t : v)//拷贝构造不被允许,&
	{
		t = thread([m] {
			for (size_t i = 0; i < m; i++)
			{
				cout << this_thread::get_id() << ":" <<i<< endl;
			}
			});
	}
	for (auto& t : v)
	{
		t.join();
	}
	return 0;
}

线程安全

两个线程对同一个变量进行加加

static int val = 0;
void fun1(int n)
{
	for (int i = 0; i < n; i++)
	{
		val++;
	}
}
void fun2(int n)
{
	for (int i = 0; i < n; i++)
	{
		val++;
	}
}
int main()
{
	thread t1(fun1, 100000);
	thread t2(fun2, 200000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

image-20230503122925800

结果本来是应该加到300000,现在却是没有,这是因为++的操作不是原子性的。

为了解决这个问题:我们可以选择进行加锁

  • 加锁
#include <mutex>
static int val = 0;
mutex mtx;
void fun1(int n)
{
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		val++;
	}
	mtx.unlock();
}

void fun2(int n)
{
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		val++;
	}
	mtx.unlock();
}

int main()
{
	thread t1(fun1, 1000000);
	thread t2(fun2, 2000000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

image-20230503123103017

注意加锁的位置:加锁与解锁的位置可以放在for循环内也可以放循环外,那到底选择哪个位置比较好:加锁与解锁也是有消耗的,所以加锁和解锁放在for循环外边比较好

当然两个线程也是可以调用同一个函数的,这是因为每个线程都会有独立的栈结构来保存私有数据,数据不会互相干扰

image-20230503123913135

  • 原子性操作
#include <atmoic>
atomic<int> aval = 0;
void func1(int n)
{
	for (size_t i = 0; i < n; i++)
	{
		++aval;
	
	}
}
int main()
{
	int m = 1000000;
	thread t1(func1, 2 * m);
	thread t2(func1, m);

	t1.join();
	t2.join();
	cout << aval << endl;
	return 0;
}

这里有可能val++会被放弃,以此来保证线程安全而在实际中要尽量避免使用全局变量。

int main()
{
	int m = 1000000;
	atomic<int> aval = 0;
	auto func = [&aval](int n){
		for (int i = 0; i < n; i++)
		{
			++aval;
		}
	};
	thread t1(func, m * 2);
	thread t2(func, m);
	t1.join();
	t2.join();
	cout << aval << endl;
	return 0;
}
  • CAS操作

原子操作是CAS提供的,有相关的接口,CAS全称compare and swap是一种原子操作,多线程非阻塞地对共享资源进行修改,但是同一时刻只有一个线程可以修改,基本原理是:先比较内存中某个值是否等于预期值,如果相等,则将新值写入内存,并返回成功;否则什么也不做,并返回失败。

lock与try_lock的区别

lock的加锁过程:如果没有锁就申请锁,如果其他线程持有锁就会阻塞等待,直到其他线程unlock。
try_lock就可以不让线程阻塞,如果申请不了就可以去干其他的事情。成功返回true,失败返回false。

recursive_mutex

递归互斥锁:如果在递归函数中我们想要正常用lock加锁,很可能能会导致死锁。因为上锁后递归到下一层,锁并没有被解开,相当于自己上了锁以后又申请锁。使用recursive_mutex就可以避免这种情况。递归到下一层后遇到加锁,就先判断线程的id值,如果一样就不用加锁,直接走接下来的流程。

lock_guard RAII锁

RAII:RAII是一种C++编程中的技术,用于管理资源的生命周期,RAII在构造函数中获取资源,并在构造函数中释放资源,以此确保使用资源的对象总是处于有效状态的,这种方式减少内存泄漏的风险。

加锁后会抛出异常,那么就可能会导致锁没有被释放。为了避免这种情况,我们可以把锁封装一下,在析构函数中就可以加上解锁,这样出了作用域就可以自动销毁。具体实现:mutex的封装

当然C++线程库中也给我们提供了这样一把锁lock_guard:

image-20230503135931291

int main()
{
	int val = 0;
	mutex mtx;
	auto func = [&](int n) {
		lock_guard<mutex> lock(mtx);
		for (int i = 0; i < n; i++)
		{
			val++;
		}
	};
	thread t1(func, 100000);
	thread t2(func, 200000);
	t1.join();
	t2.join();
	cout << val << endl;
	return 0;
}

image-20230503140237881

unique_lock主动解锁

image-20230503141032150

与lock_guard的区别就是lock_guard只能实现RAII,lock_guard 在构造时就自动获取锁,在析构时自动释放锁,更适合对简单场景进行上锁和解锁操作;而 unique_lock 则允许在任何时候手动控制锁的加锁和解锁

实现两个线程交替打印1-100

尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。

if判断实现交替写法:

int main()
{
	int i = 0;
	thread t1([&i] {
		while (i < 100)
		{
			if (i % 2)
			{
				cout << this_thread::get_id() << "->" << i << endl;
				i++;
			}
		}
		});
	thread t2([&i] {
		while (i <= 100)
		{
			if (i % 2 == 0)
			{
				cout << this_thread::get_id() << "->" << i << endl;
				i++;
			}
		}
		});
	t1.join();
	t2.join();
	return 0;
}

第二个while加上=,虽然这可以做到要求,但是可能会造成资源浪费。可能有一种场景:当我们的t2满足条件正在运行,但是时间片到了,切换到t1,此时t1不满足条件,一直在while处死循环,直到时间片到了才切换出去,导致浪费占用CPU资源。所以我们希望两个线程能够相互通知,这就需要条件变量控制。

条件变量

条件变量的概念在线程同步——条件变量一文中我们介绍了

C++11也对条件变量进行了封装。头文件:

#include<condition_variable>

相关的接口:

image-20230503143904677

image-20230503144031405

条件变量不是线程安全的,要与锁互相配合使用。

int main()
{
	int i = 0;
	mutex mtx;
	condition_variable cv;
	thread t1([&] {
		while (i < 100)
		{
			unique_lock<mutex> lock(mtx);
			while (i % 2 == 0)//偶数
			{
				cv.wait(lock);
			}
			cout << "t1: " << this_thread::get_id() << "->" << i << endl;
			i++;
			cv.notify_one();
		}
	});
	thread t2([&] {
		while (i <= 100)
		{
			unique_lock<mutex> lock(mtx);
			while (i % 2)
			{
				cv.wait(lock);
			}
			cout << "t2: " << this_thread::get_id() << "->" << i << endl;
			i++;
			cv.notify_one();
		}
		});
	t1.join();
	t2.join();
	return 0;
}

image-20230503144832046

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

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

相关文章

python函数的递归调用

引入 函数既可以嵌套定义也可以嵌套调用。嵌套定义指的是在定义一个函数时在该函数内部定义另一个函数&#xff1b;嵌套调用指的是在调用一个函数的过程中函数内部有调用另一个函数。而函数的递归调用指的是在调用一个函数的过程中又直接或者间接的调用该函数本身。 函数递归…

Python入门(三)变量和简单数据类型(二)

变量和简单数据类型&#xff08;二&#xff09; 1.数1.1 整数操作1.2 浮点数操作1.3 整数和浮点数1.4 数中的下划线1.5 同时给多个变量赋值1.6 常量 2.注释2.1 如何编写注释2.2 编写什么样的注释 作者&#xff1a;Xiou 1.数 数在编程中是经常使用的一种数据类型&#xff0c;可…

【目标检测论文阅读笔记】Dynamic Head: Unifying Object Detection Heads with Attentions

Abstract 在目标检测中结合定位和分类的复杂性导致了方法的蓬勃发展。以前的工作试图提高各种目标检测头的性能&#xff0c;但未能提出统一的观点。在本文中&#xff0c;我们提出了一种新颖的动态头部框架 来统一目标检测头部和注意力。通过在用于尺度感知的特征级别之间、用于…

嵌入式Linux:FrameBuffer 和 DRM/KMS(一)

文章目录 前言: Linux 的两种显示方案FrameBufferDRM1、GEM2、KMS 参考&#xff1a;RK3399 探索之旅 / Display 子系统 / 基础概念 参考&#xff1a;DRM架构介绍&#xff08;一&#xff09; 前言: Linux 的两种显示方案 包括&#xff1a; FBDEV: Framebuffer Device DRM/KM…

【MediaSoup c#】 worker的创建

js rust 不太熟,c# 似乎还好懂一些。学习media soup 的各个组件及大体使用方式学习其设计理念。MediasoupServer 管理worker列表 worker的表达是通过 IWorker 抽象类 拥有一个observer 实例 (EventEmitter): /// <summary>/// Observer instance./// </summary&g…

顺序表和链表优缺点以及区别

顺序表和链表的区别 顺序表优点缺点 链表优点缺点 顺序表和链表不同点 顺序表 优点 1.尾插尾删效率高 2.支持随机访问 3/相比于链&#xff0c;cpu高速缓存命中率更高 缺点 1.在头部和中部插入删除效率底 2.需要大片连续空间&#xff0c;改变容量不方便 链表 优点 1.不需要…

【嵌入式Linux驱动】驱动开发调试相关的关系记录

https://www.processon.com/mindmap/64537772b546c76a2f37bd2f

MySQL概述 -- 数据模型SQL简介DDL数据库操作

一. 数据模型 介绍完了Mysql数据库的安装配置之后&#xff0c;接下来我们再来聊一聊Mysql当中的数据模型。学完了这一小节之后&#xff0c;我们就能够知道在Mysql数据库当中到底是如何来存储和管理数据的。 在介绍 Mysql的数据模型之前&#xff0c;需要先了解一个概念&#x…

SPSS如何进行对应分析之案例实训?

文章目录 0.引言1.对应分析2.多重对应分析 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对SPSS进行了学习&#xff0c;本文通过《SPSS统计分析从入门到精通》及其配套素材结合网上相关资料进行学习笔记总结&#xff0c;本文对对应分析进行阐述。 1.对应分析 &#…

混合策略改进的金枪鱼群优化算法(HTSO)-附代码

混合策略改进的金枪鱼群优化算法(HTSO) 文章目录 混合策略改进的金枪鱼群优化算法(HTSO)1.金枪鱼群优化算法2. 改进金枪鱼群优化算法2.1 Circle混沌映射初始化种群Circle2.2 Levy flight改进螺旋式觅食 3.实验结果4.参考文献5.Matlab代码6.Python代码 摘要&#xff1a;针对金枪…

操作系统考试复习——第三章 进程调度和实时调度

进程调度的方式分为&#xff1a;抢占式和非抢占式 采用非抢占式时&#xff0c;一旦把处理机分配给某进程后&#xff0c;就让他一直运行下去&#xff0c;决不会因为时钟中断或其他任何原因去抢占当前正在运行进程的处理机。直至该进程完成或因为某件事情堵塞&#xff0c;才把处…

MMediting1.X进行视频超分训练和测试(BasicVsr++)

因为MMediting更新了版本&#xff0c;整体的变化比较大&#xff0c;导致之前的一些介绍操作的帖子不太适合新手入门&#xff0c;这里以作者自己对BasicVsr模型进行测试和训练的过程&#xff0c;写一下具体的操作过程。 &#xff08;1&#xff09;找到模型     首先在config…

JavaScript判断是否为NaN

&#xff08;旅行是最劳顿&#xff0c;最麻烦&#xff0c;叫人本相必现的时候。经过长期苦旅行而彼此不讨厌的人&#xff0c;才可以结交作朋友。——钱钟书&#xff09; 为什么NaN在js值得一提 相信你在实际开发中&#xff0c;经常遇到一下场景 const pasNumber parseInt(un…

财报解读:照明行业景气上行,欧普照明已步入增长“快车道”

2023年&#xff0c;随着文旅景观照明复苏&#xff0c;教室照明市场蓬勃发展&#xff0c;双碳战略下照明数字化系统加速建设&#xff0c;照明企业迎来行业曙光。近日&#xff0c;A股绿色照明龙头企业欧普照明股份有限公司&#xff08;下称“欧普照明”&#xff0c;股票代码60351…

刷题记录˃ʍ˂

一、1033. 移动石子直到连续 思路 这道题是一道数学题&#xff0c;它一共分为三种可能 第一种可能为三个石子本来就是连续的时候 第二种可能为最少步数为1的时候&#xff0c;相邻石子不能大于一格 第三种可能为最少步数为2的时候&#xff0c;这时相邻石子大于一格 那么第二…

华为MPLS跨域C1方式RR场景(数据经过RR)实验配置

目录 配置BGP邻居的建立 配置MPLS LDP 配置RR之间的MP-BGP邻居 根据图配置接口的IP地址和IGP协议 配置BGP邻居的建立 PE1和RR1建立IBGP邻居、RR1和ASBR1建立IBGP邻居&#xff08;RR1作为反射器&#xff09; PE1和RR1建立MP-IBGP邻居&#xff08;RR1传给PE1的Vpnv4路由要求更…

2023 hnust 大三下 人工智能导论课程 期中考试复习笔记

前言 ★大概率考✦个人推测考点※补充内容没有完全覆盖“人工智能导论复习2023.pdf”的重点致谢&#xff1a;hwl、lyf、lqx 题型 问答&#xff1a;5*10分综合&#xff1a;15分设计&#xff1a;25分开放题/论述题&#xff1a;10分 第1章 绪论 人工智能的定义 智能 思考与…

Android ADB安装apk失败:INSTALL_FAILED_TEST_ONLY

droid ADB安装apk失败:INSTALL_FAILED_TEST_ONLY 解决&#xff1a; 1、一种是在代码里面加&#xff0c; 在gradle.properties配置文件加入属性&#xff0c;把testOnly设置为false Android安装apk失败&#xff1a;The application could not be installed: INSTALL_FAILED_TES…

Java内存模型之JMM

计算机硬件存储系统 因为有这么多级的缓存&#xff08;cpu和物理主内存的速度不一致&#xff09; CPU的运行并不是直接操作内存而是把内存里边的数据读取到缓存&#xff0c;而内存的读和写操作的时候就会造成不一致的问题 JVM规范中试图定义一种Java的内存模型&#xff08;ja…

【算法与数据结构】递归函数设计技巧

数学归纳法 step1: 验证P(1)成立step2: 证明如果P(k)成立&#xff0c;那么P(k1)也成立step3: 联合step1和step2&#xff0c;证明由P(1)->P(n)成立 例1&#xff1a; 证明&#xff1a; 1 3 . . . ( 2 n − 1 ) n 2 13...(2n-1) n^2 13...(2n−1)n2 证明P(1)成立&#…