C++11---多线程

news2024/11/19 20:33:18

看前须知:如果对线程不了解的,可以先去看Linux---多线程(上),(下)这两篇文章

那里主要讲了线程的一些基础概念和底层相关理解,对我们阅读这篇文章会有所帮助

一、thread --- 线程

1、thread相关接口介绍

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接 口,这使得代码的可移植性比较差C++11中最重要的特性就是对线程进行支持了,使得C++在 并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件
函数接口功能说明
thread()
构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...)
构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的参数
get_id()
获取线程id
joinable()
查看线程是否是连接状态,与detch后的线程的分离状态相对应
join()
该函数调用后会阻塞等待线程结束
如果线程是默认构造的线程对象 / 已经被detach / 已经被 join,再调用join会出错
detach()
在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关

下面是一个简单的创建线程的代码

#include<thread>
#include<iostream>

using namespace std;

void Print(size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		cout << i << " ";
	}
	cout << endl;
}


int main()
{
	thread t(Print, 10);
	// t.detach();
	if (t.joinable())
		t.join();
	cout << t.joinable() << endl;
	return 0;
}

注意:

  • 线程是操作系统中的概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态
  • 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
  • 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供: 函数指针lambda表达式、仿函数、包装器,如下
#include<iostream>
void Print(size_t n)
{
	cout << this_thread::get_id() << " : "; // 获取当前线程的线程id
	for (size_t i = 0; i < n; i++)
	{
		cout << i << " ";
	}
	cout << endl;
}

struct print
{
	void operator()(size_t n)
	{
		cout << this_thread::get_id() << " : ";
		for (size_t i = 0; i < n; i++)
		{
			cout << i << " ";
		}
		cout << endl;
	}
};

int main()
{
	thread t1(Print, 10);
	Sleep(1);
	thread t2(print(), 20);
	Sleep(1);
	thread t3([](size_t n) {
		cout << this_thread::get_id() << " : ";
		for (size_t i = 0; i < n; i++)
		{
			cout << i << " ";
		}
		cout << endl;
	}, 30);
	Sleep(1);
	function<void(size_t)>f = [](size_t n) {
		cout << this_thread::get_id() << " : ";
		for (size_t i = 0; i < n; i++)
		{
			cout << i << " ";
		}
		cout << endl;
	};

	thread t4(f, 40);
	t1.join();
	t2.join();
	t3.join();
	t4.join();
	return 0;
}
  • thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

void Print(size_t n, string s)
{
	cout << s << " : ";
	for (size_t i = 0; i < n; i++)
	{
		cout << i << " ";
	}
	cout << endl;
}

int main()
{
	int n = 10;
	vector<thread> vthd(n);
	size_t j = 0;
	for (auto& thd : vthd)
	{
        // 移动赋值 --- 临时变量是将亡值
		thd = thread(Print, 10, "线程" + to_string(j++));
		Sleep(1);// 休眠的目的是为了让打印出来的数据看起来不乱
	}
	for (auto& thd : vthd)
	{
		thd.join();
	}
    thread t1(Print, 10, "zwxs");
	// thread t2(t1); // 错,thread不支持拷贝构造
	thread t2(move(t1)); //thread支持移动构造

	t2.join();
	return 0;
}

  • 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
  1. 采用无参构造函数构造的线程对象
  2. 线程对象的状态已经转移给其他线程对象
  3. 线程已经调用jion或者detach结束

2、线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此,即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参,如果不理解,可以直接记住结论,即创建线程时传引用得加std::ref()

void Print(size_t n, int & x)
{
	for (size_t i = 0; i < n; i++)
	{
		x++;
	}
}

void Print1(size_t n, int* x)
{
	for (size_t i = 0; i < n; i++)
	{
		(*x)++;
	}
}

int main()
{
	int x = 0;
	// thread t(Print, 10, x); // 会报错
	thread t(Print, 10, ref(x)); // std::ref() 帮助我们传递引用
	t.join();
	cout << x << endl;

	thread t1(Print1, 10, &x); // 可以直接传指针,通过指针来修改值
	t1.join();
	cout << x << endl;
	return 0;
}

3、this_thread命名空间

this_thread 是 C++11 引入的一个命名空间,它位于 std 下,并提供了一组函数,用于操作当前执行的线程。这个命名空间的目的是为开发者提供一种便捷的方式来获取和管理当前线程的信息和行为。

  1. get_id:这个函数用于获取当前线程的线程 ID。线程 ID 是一个唯一标识线程的整数值,通过它可以在程序中区分和追踪不同的线程。
  2. yield:这个函数用于让当前线程主动放弃处理器的使用权,使得其他线程有机会执行。这是一种线程间的协作机制,有助于实现更高效的线程调度。
  3. sleep_for:这个函数使当前线程进入休眠状态,直到指定的时间段过去,这可以用于控制线程的执行节奏,或者在某些情况下,等待某些条件成立。
  4. sleep_until:这个函数使当前线程进入休眠状态,直到达到指定的时间点。它允许线程在特定的时间唤醒并执行。

与时间相关的函数可以结合<chrono>头文件下的相关函数使用,如hours,minutes,seconds等

void Print(size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		cout << i << " ";
		this_thread::sleep_for(chrono::seconds(1));
	}
}

二、mutex --- 锁

在C++11中,Mutex总共包了四个互斥量的种类,都不支持拷贝构造/移动拷贝/赋值拷贝/移动赋值

1、std::mutex

C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用
的三个函数:
函数接口功能说明
lock()阻塞加锁
try_lock()非阻塞加锁
unlock()解锁

int main()
{
	int x = 0;
	int n = 1000;
	thread t1([&]() {
		for (int i = 0; i < n; i++)
		{
			x++;
		}
	});

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

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

	cout << x << endl;
	return 0;
}

很明显,上面的代码是线程不安全的,我们需要给线程加锁,代码如下

int main()
{
	int x = 0;
	int n = 1000;
	mutex mtx;
	thread t1([&]() {
		for (int i = 0; i < n; i++)
		{
			mtx.lock();
			x++;
			mtx.unlock();
		}
	});

	thread t2([&]() {
		for (int i = 0; i < n; i++)
		{
			mtx.lock();
			x++;
			mtx.unlock();
		}
	});

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

	cout << x << endl;
	return 0;
}

这里细心的读者可能已经发现了一个问题:为什么这里能"传"引用?注意:这里不是传参,而是lambda表达式的捕获列表,可以理解为两者底层走的不是一个逻辑,所以这里可以,至于具体底层是如何走的,有兴趣的可以自己去查查看。

2、std::recursive_mutex

允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

3、std::timed_mutex

比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

4、std::recursive_timed_mutex

具有recursive_mutex和timed_mutex的特性


出现死锁的情况,如下

void func(int x)
{
	if (x%2)
		throw exception("异常");
	else
		cout << "func()" << endl;
}

int main()
{
	mutex mtx;
	int y;
	thread t1([&]() {
		try {
			for (int i = 0; i < 10; i++) {
				mtx.lock();
				y++;
				func(i);
				mtx.unlock();
			}
		}
		catch (const exception& e) {
			cout << e.what() << endl;
		}
	});

	thread t2([&]() {
		try {
			for (int i = 0; i < 10; i++) {
				mtx.lock();
				y++;
				func(i);
				mtx.unlock();
			}
		}
		catch (const exception& e) {
			cout << e.what() << endl;
		}
		});
	t1.join();
	t2.join();
	return 0;
}

针对上面的情况,我们需要有一个能自动释放的锁,类似智能指针,库给我们提供了lock_guard和unique_lock用来封装锁,不需要我们去手动释放锁。

thread t1([&]() {
	try {
		for (int i = 0; i < 10; i++) {
			lock_guard<mutex> lock(mtx);
			y++;
			func(i);
		}
	}
	catch (const exception& e) {
		cout << e.what() << endl;
	}
});

lock_guard和unique_lock的区别:

lock_guard只支持构造和析构,没有其他功能

unique_lock能支持手动的加锁和解锁,并且能用时间进行控制

三、condition_variable --- 条件变量

不了解的可以先去看Linux---多线程(下)

常用的三个函数接口:

函数接口功能说明
void wait(unique_lock<mutex>& lck)等待条件就绪,再往下执行,会先释放申请到的锁,故只能传unique_lock,lock_guard不支持手动加锁和解锁
void notify_one()唤醒在条件变量的等待队列中的一个线程,需要重新加锁
void notify_all()唤醒在条件变量的等待队列中的所有线程,需要重新加锁
如何实现两个线程交替打印,一个打印奇数,一个打印偶数?
int main()
{
	int x = 1;
	condition_variable cv;
	mutex mtx;
	thread t1([&]() {
		for (int i = 0; i < 10; i++) {
			unique_lock<mutex> lock(mtx);
			while (x%2==0)
				cv.wait(lock);
			cout << this_thread::get_id() << " : " << x++ << endl;
			cv.notify_one();
		}
	});

	thread t2([&]() {
		for (int i = 0; i < 10; i++) {
			unique_lock<mutex> lock(mtx);
			while (x%2)
				cv.wait(lock);
			cout << this_thread::get_id() << " : " << x++ << endl;
			cv.notify_one();
		}
	});

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

四、atomic

C++11中的<atomic>库提供了对原子操作的支持,这些操作在多线程环境中是线程安全的。原子操作是不可中断的操作,即在执行完毕之前不会被其他线程打断。通过使用原子操作,我们可以避免使用互斥量(mutexes)和条件变量(condition variables)等同步原语,从而在某些情况下提高性能并简化代码

比如上面用锁保证x++的线程安全,其实比较浪费资源,因为申请锁一旦失败,线程就会阻塞,需要让出cpu,切换上下文数据等,而我们加锁只是为了执行x++这一条语句,显然很不值得,这里我们就可以用atomic中的函数,进行原子操作,不需要进入阻塞,代码如下

int main()
{
	atomic<int> x = 0;
	int n = 1000;
	//mutex mtx;
	thread t1([&]() {
		for (int i = 0; i < n; i++)
		{
			x++;// 这里的++用的是运算符重载
		}
	});

	thread t2([&]() {
		for (int i = 0; i < n; i++)
		{
			x++;// 这里的++用的是运算符重载
		}
	});

	t1.join();
	t2.join();
	std::cout << x << std::endl;
	return 0;
}

可以简单说明一下,它用的无锁编程---利用原子操作实现锁的功能。上面的++操作符就是用CAS (compare and swap)这个原子操作实现的(CAS在不同的语言中都会有对应的函数)

/*伪代码
int x = 0;
int old,newval;
do
{
	old = x;
	newval = old + 1;
}while(!CAS(&x,&old,newval)) 
//看x在内存中的值是否和old相同,如果相同*x=newval,返回true,否则返回false
*/

如果对无锁编程感兴趣可以去查查文档,这里就不多做介绍了,这个对编程能力要求有点高,建议最好不要轻易去写。

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

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

相关文章

汇编语言程序设计-1-绪论

1. 绪论 文章目录 1. 绪论1.1 导学1.2 为什么要学汇编语言1.3 由机器语言到汇编语言1.4 计算机的组成1.5 内存的读写与地址空间1.6 汇编语言实践环境搭建 参考视频&#xff1a;烟台大学贺利坚老师的网课《汇编语言程序设计系列专题》&#xff0c;或者是B站《汇编语言程序设计 贺…

MybatisPlus实现数据权限隔离

引言 Mybatis Plus对Mybatis做了无侵入的增强&#xff0c;非常的好用&#xff0c;今天就给大家介绍它的其中一个实用功能&#xff1a;数据权限插件。 数据权限插件的应用场景和多租户的动态拦截拼接SQL一样。建议点赞收藏关注&#xff0c;方便以后复习查阅。 依赖 首先导入M…

c/c++普通for循环学习

学习一下 for 循环的几种不同方式&#xff0c;了解一下原理及差异 完整的测试代码参考 GitHub &#xff1a;for 循环测试代码 1 常用形态 对于 for 循环来说&#xff0c;最常用的形态如下 for (表达式1; 表达式2; 表达式3) {// code }流程图如下&#xff1a; 编写测试代码…

【随笔】Git 高级篇 -- 项目里程碑 git tag(二十)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

12 Php学习:魔术常量

PHP魔术常量 PHP 向它运行的任何脚本提供了大量的预定义常量。 不过很多常量都是由不同的扩展库定义的&#xff0c;只有在加载了这些扩展库时才会出现&#xff0c;或者动态加载后&#xff0c;或者在编译时已经包括进去了。 有八个魔术常量它们的值随着它们在代码中的位置改…

Java——类和对象

目录 一.类定义和使用 1.简单认识类 2.类的定义格式 3.注意事项 二.课堂练习 1.定义一个狗类 2.定义一个学生类 3.注意事项&#xff1a; 三.类的实例化 1.什么是实例化 2.注意事项 3.类和对象的说明 四.this引用 1.为什么要有this引用 2.什么是this引用 五.对…

MySQL进阶二

目录 1.使用环境 2.排序窗口函数 3.聚合窗口函数 1.使用环境 数据库&#xff1a;MySQL 8.0.30 客户端&#xff1a;Navicat 15.0.12 接续MySQL进阶一&#xff1a; MySQL进阶一-CSDN博客文章浏览阅读452次&#xff0c;点赞9次&#xff0c;收藏4次。MySQL进阶操作一。https…

ThignsBoard通过服务端订阅共享属性

MQTT基础 客户端 MQTT连接 通过服务端订阅属性 案例 1、首先需要创建整个设备的信息&#xff0c;并复制访问令牌 ​​2、通过工具MQTTX连接上对应的Topic 3、测试链接是否成功 4、在MQTT上订阅对应的Topic 5、在客户端添加共享属性信息 6、查看整个设备的遥测数据 M…

Qt 窗⼝

Qt 窗⼝ 菜单栏创建菜单栏在菜单栏中添加菜单创建菜单项在菜单项之间添加分割线综合⽰例 ⼯具栏创建⼯具栏设置停靠位置设置浮动属性设置移动属性综合⽰例状态栏状态栏的创建在状态栏中显⽰实时消息在状态栏中显⽰永久消息 浮动窗⼝浮动窗⼝的创建设置停靠的位置 对话框对话框介…

Spring声明式事务控制

文章目录 1.编程式事务控制相关对象(了解即可)1.1PlarformTransactionManager1.2 TransactionDefinition 2.基于XML的声明式事务控制2.1声明式事务控制的实现 3.基于注解的声明式事务控制 1.编程式事务控制相关对象(了解即可) 1.1PlarformTransactionManager PlatformTransac…

NL2SQL实践系列(1):深入解析Prompt工程在text2sql中的应用技巧

NL2SQL实践系列(1)&#xff1a;深入解析Prompt工程在text2sql中的应用技巧 NL2SQL基础系列(1)&#xff1a;业界顶尖排行榜、权威测评数据集及LLM大模型&#xff08;Spider vs BIRD&#xff09;全面对比优劣分析[Text2SQL、Text2DSL] NL2SQL基础系列(2)&#xff1a;主流大模型…

LLM应用开发框架LangChain

1、LangChain简介 1.1、LangChain发展史 LangChain 的作者是 Harrison Chase&#xff0c;最初是于 2022 年 10 月开源的一个项目&#xff0c;在 GitHub 上获得大量关注之后迅速转变为一家初创公司。2017 年 Harrison Chase 还在哈佛上大学&#xff0c;如今已是硅谷的一家热门…

314_C++_QT表格的撤销、恢复,可对多行、多item进行撤销、恢复操作

行–删除后的,撤销、恢复图示: 原图示 删除后 撤销操作 恢复操作 item修改后的撤销、恢复 原item 撤销修改 恢复修改 代码: --</

vue快速入门(二十四)输入停顿再进行响应

注释很详细&#xff0c;直接上代码 上一篇 新增内容 使用侦听器监视数据变化情况使用clearTimeout与定时器实现停顿一段时间再操作内容 源码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"view…

Chatgpt掘金之旅—有爱AI商业实战篇|播客剧本写作|(十三)

演示站点&#xff1a; https://ai.uaai.cn 对话模块 官方论坛&#xff1a; www.jingyuai.com 京娱AI 一、AI技术创业播客剧本写作服务有哪些机会&#xff1f; 人工智能&#xff08;AI&#xff09;技术作为当今科技创新的前沿领域&#xff0c;为创业者提供了广阔的机会和挑战。…

代码随想录算法训练营第三十九天|62.不同路径、63.不同路径II

代码随想录算法训练营第三十九天|62.不同路径、63.不同路径II 62.不同路径 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为…

nginx反向代理conf

打开nginx配置。 对登录功能测试完毕后&#xff0c;接下来&#xff0c;我们思考一个问题&#xff1a;前端发送的请求&#xff0c;是如何请求到后端服务的&#xff1f; 前端请求地址&#xff1a;http://localhost/api/employee/login 后端接口地址&#xff1a;http://localho…

[leetcode]maximum-width-of-binary-tree

. - 力扣&#xff08;LeetCode&#xff09; 给你一棵二叉树的根节点 root &#xff0c;返回树的 最大宽度 。 树的 最大宽度 是所有层中最大的 宽度 。 每一层的 宽度 被定义为该层最左和最右的非空节点&#xff08;即&#xff0c;两个端点&#xff09;之间的长度。将这个二叉…

使用python编写网页自动答题-仿真考试

自动化实践经验分享 监听数据包地址&#xff1a;通过监听数据包地址&#xff0c;可以获得实时的答案信息&#xff0c;确保答题的准确性和实效性。提取答案内容&#xff1a;使用正则表达式和json模块&#xff0c;可以快速提取和处理答案信息。答题操作&#xff1a;根据答案内容…

前端三剑客 —— JavaScript (第八节)

目录 内容回顾&#xff1a; 事件对象 事件对象 事件对象的方法和属性 案例-移动DIV 案例-图片轮换 Ajax 内容回顾&#xff1a; 事件对象 1.1 什么是事件驱动 1.2 事件绑定 事件源&#xff1a;发生事件的源对象 事件对象&#xff1a;它包含了事件所有的信息&#xff0c;它…