《C++11:通过thread类编写C++多线程程序》

news2025/4/1 16:15:06

关于多线程的概念与理解,可以先了解Linux下的底层线程。当对底层线程有了一定程度理解以后,再学习语言级别的多线程编程就轻而易举了。

【Linux】多线程 -> 从线程概念到线程控制

【Linux】多线程 -> 线程互斥与死锁


语言级别的多线程编程最大的好处就是可以跨平台,使用语言级别编写的多线程程序不仅可以在Windows下直接编译运行,在Linux下也是可以直接编译运行的。其实本质还是调用了不同操作系统的底层线程API。

多线程简单使用

  • 怎么创建启动一个线程?

#include<iostream>
#include<thread>
#include<chrono>

void threadHandle1(int time)
{
	// 让线程休眠两秒
	std::this_thread::sleep_for(std::chrono::seconds(time));
	std::cout << "hello thread1" << std::endl;
}

int main()
{
	// 创建一个线程对象,传入线程函数以及线程函数所需要的参数,线程就开始执行了
	std::thread t1(threadHandle1, 2);

	// 主线程等待子线程结束,再继续往下运行
	t1.join();

	std::cout << "main thread done!" << std::endl;
	return 0;
}

主线程要t1.join()等待子线程结束之后才能向后运行。如果不等待,就会异常终止。

  • 子线程如何结束?

子线程函数运行完成就结束了。

也可以将线程设置为分离状态,主线程就可以不用等待子进程执行完再继续向后运行了。

void threadHandle1(int time)
{
	// 让线程休眠两秒
	std::this_thread::sleep_for(std::chrono::seconds(time));
	std::cout << "hello thread1" << std::endl;
}

int main()
{
	// 创建一个线程对象,传入线程函数以及线程函数所需要的参数,线程就开始执行了
	std::thread t1(threadHandle1, 2);

	// 主线程等待子线程结束,再继续往下运行
	// t1.join();

	// 设置线程为分离状态
	t1.detach();

	std::cout << "main thread done!" << std::endl;
	return 0;
}

  •  主线程如何处理子线程?

1、t1.join()等待线程,等待线程结束后主线程再继续向后运行。

2、t1.detach()分离线程,主线程结束,整个进程就结束了,所有线程都自动结束了。


#include<iostream>
#include<thread>
#include<chrono>

void threadHandle1(int time)
{
	// 让线程休眠time秒
	std::this_thread::sleep_for(std::chrono::seconds(time));
	std::cout << "hello thread1" << std::endl;
}

void threadHandle2(int time)
{
	// 让线程休眠time秒
	std::this_thread::sleep_for(std::chrono::seconds(time));
	std::cout << "hello thread1" << std::endl;
}

int main()
{
	// 创建一个线程对象,传入线程函数以及线程函数所需要的参数,线程就开始执行了
	std::thread t1(threadHandle1, 2);
	std::thread t2(threadHandle2, 2);
	// 主线程等待子线程结束,再继续往下运行
	t1.join();
	t2.join();
	// 设置线程为分离状态
	// t1.detach();

	std::cout << "main thread done!" << std::endl;
	return 0;
}

 mutex互斥锁和lock_guard

#include<iostream>
#include<thread>
#include<list>

int tickets = 1000;

void getTicket(int i)
{
	// 模拟用户抢票的行为
	while (tickets > 0)
	{
		std::cout << "用户:" << i << "正在进行抢票!" << tickets << std::endl;
		tickets--;
		std::this_thread::sleep_for(std::chrono::microseconds(100));
	}
}

int main()
{
	std::list<std::thread> list;
	for (int i = 1; i <= 3; i++)
	{
		list.push_back(std::thread(getTicket, i));
	}
	for (std::thread &t: list)
	{
		t.join();
	}

	return 0;
}

输出的结果杂乱无章,并且抢票出现负数。

多线程对临界区的访问会存在竞态条件:临界区代码段在多线程环境下执行,随着线程的调度时许不同,从而产生不同的结果。所以,我们要保证对临界区的原子操作,是通过对临界区加锁完成的。

由于多个线程同时向标准输出流std::cout输出信息,会造成输出混乱,各线程的输出可能相互穿插。

在多线程环境下,多个线程同时访问和修改共享资源tickets,会引发数据竞争问题。比如,当一个线程检查到tickets>0后,在执行tickets--操作之前,另一个线程也可能检查到tickets>0,进而导致重复售票或者出现负数票数的情况。

++/--操作并不是原子性的,其实是对应三条汇编指令完成的。

  • 读取:从内存中把变量的值读取到寄存器
  • 修改:在寄存器里将变量的值+1/-1
  • 写入:把修改后的值写入到内存

在单线程环境下,这三个步骤顺序执行不会有问题。但是在多线程环境下,多个线程可能对同一个变量同时进行++/--操作,从而导致数据竞争的问题。

可以看下面的过程演示。

一:

二:

三:

C++11是通过加锁来保证++/--操作的原子性的。


#include<iostream>
#include<thread>
#include<list>
#include<mutex>

int tickets = 1000;
std::mutex mtx; // 全局的一把锁

void getTicket(int i)
{
	// 模拟用户抢票的行为
	while (tickets > 0)
	{
		mtx.lock();
		// 锁+双重判断,如果不双重判断,当tickets为1时,用户正在抢票还没有执行到tickets--,其他用户判断tickets>0,也进来了等待锁。
		// 用户抢票之后,其他用户获取到锁还会进行抢票。
		if (tickets > 0)
		{
            // 临界区代码段 - 加锁保护
			std::cout << "用户:" << i << "正在进行抢票!" << tickets << std::endl;
			tickets--;
			mtx.unlock();
		}
	}
}

int main()
{
	std::list<std::thread> list;
	for (int i = 1; i <= 3; i++)
	{
		list.push_back(std::thread(getTicket, i));
	}
	for (std::thread &t: list)
	{
		t.join();
	}

	return 0;
}

通过加锁和双重判断的方式,这样就能做到对多线程对共享资源tickets的安全访问了。

lock_guard和unique_lock

其实不需要我们手动加锁和解锁,因为,如果临界区内有return,break等语句,此线程获取锁,但是在释放锁之前break或者renturn了,导致锁没有释放。那么其他线程也获取不了锁,就会造成死锁问题,所以对于这把互斥锁要有RAII的思想。

使用lock_guard。构造函数获取锁,析构函数释放锁。

int tickets = 1000;
std::mutex mtx; // 全局的一把锁

void getTicket(int i)
{
	// 模拟用户抢票的行为
	while (tickets > 0)
	{
		{
			std::lock_guard<std::mutex> lock(mtx);
			// mtx.lock();
			// 锁+双重判断,如果不双重判断,当tickets为1时,用户正在抢票还没有执行到tickets--,其他用户判断tickets>0,也进来了等待锁。
			// 用户抢票之后,其他用户获取到锁还会进行抢票。
			if (tickets > 0)
			{
				std::cout << "用户:" << i << "正在进行抢票!" << tickets << std::endl;
				tickets--;
			}
			// mtx.unlock();
		}
		std::this_thread::sleep_for(std::chrono::microseconds(100));
	}
}

不管临界区是正常执行,还是break或return了,出了作用域,lock_guard会自动调用析构函数释放锁,保证所有线程都能释放锁,避免死锁问题发生。

lock_guard不支持拷贝构造和赋值运算符重载,如果需要拷贝和赋值可以使用unique_lock,支持移动语义,可以使用右值引用的拷贝构造和赋值运算符重载。

int tickets = 1000;
std::mutex mtx; // 全局的一把锁

void getTicket(int i)
{
	// 模拟用户抢票的行为
	while (tickets > 0)
	{
		{
			std::unique_lock<std::mutex> lck(mtx);
			// std::lock_guard<std::mutex> lock(mtx);
			// mtx.lock();
			// 锁+双重判断,如果不双重判断,当tickets为1时,用户正在抢票还没有执行到tickets--,其他用户判断tickets>0,也进来了等待锁。
			// 用户抢票之后,其他用户获取到锁还会进行抢票。
			if (tickets > 0)
			{
				std::cout << "用户:" << i << "正在进行抢票!" << tickets << std::endl;
				tickets--;
			}
			// mtx.unlock();
		}
		std::this_thread::sleep_for(std::chrono::microseconds(100));
	}
}

总结:lock_guard不能用在函数传递或返回的过程中,因为lock_guard删除了拷贝构造和赋值,只能用在简单的临界区代码段的互斥操作中。unique_guard可以用在函数传递或返回的过程中,因为支持移动语义,可以使用移动构造和移动赋值。unique_guard通常与条件变量一起使用。

线程间的同步通信

生产者-消费者模型

std::mutex mtx;
std::condition_variable cv;

// 边生产边消费
class Queue
{
public:
	void put(int val)
	{

		// 加锁
		std::unique_lock<std::mutex> lock(mtx);
		// 如果队列不为空,则等待
		while (!que.empty())
		{
			// 1.进入等待状态 2.释放锁
			cv.wait(lock);
			// 被唤醒之后,等待状态进入阻塞状态,获取锁进入就绪状态继续执行
		}
		que.push(val);
		cv.notify_all(); // 通知消费者消费
		std::cout << "生产者 生产:" << val << "号物品" << std::endl;
	}
	int get()
	{
		// 加锁
		std::unique_lock<std::mutex> lock(mtx);
		// 如果队列为空,则等待
		while (que.empty())
		{
			// 1.进入等待状态 2.释放锁
			cv.wait(lock);
			// 被唤醒之后,等待状态进入阻塞状态,获取锁进入就绪状态继续执行
		}
		int val = que.front();
		que.pop();
		cv.notify_all(); // 通知生产者生产
		std::cout << "消费者 消费:" << val << "号物品" << std::endl;

		return val;
	}
private:
	std::queue<int> que;
};


// 生产
void producer(Queue* que)
{
	for (int i = 0; i <= 10; i++)
	{
		que->put(i);
		std::this_thread::sleep_for(std::chrono::microseconds(100));
	}
}
// 消费
void conducer(Queue* que)
{
	for (int i = 0; i <= 10; i++)
	{
		que->get();
		std::this_thread::sleep_for(std::chrono::microseconds(100));
	}
}
int main()
{
	Queue que;
	std::thread p(producer, &que);
	std::thread c(conducer, &que);
	p.join();
	c.join();
	return 0;
}

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

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

相关文章

19-dfs-排列数字(基础)

题目 来源 842. 排列数字 - AcWing题库 思路 由于相对简单&#xff0c;是dfs的模板题&#xff0c;具体思路详见代码 代码 #include<bits/stdc.h> using namespace std; const int N10; int state[N],path[N];//是否使用过&#xff0c;当前位置 int n; void dfs(int …

32.代码题

接着上集...... 派对&#xff1a;超时了&#xff0c;总该受到惩罚吧&#xff1f; 洛西&#xff1a;至于吗&#xff1f;就0.1秒&#xff01; 晴/宇&#xff1a;十分应该。 洛西&#xff1a;我..................... 没办法&#xff0c;洛西只能按照要求去抓R了。 1.P1102 …

nacos 3.x Java SDK 使用详解

Nacos 3.x Java SDK 使用详解 Nacos 3.x 是云原生服务治理的重要升级版本&#xff0c;其 Java SDK 在性能、协议和扩展性上均有显著优化。 一、环境要求与依赖配置 基础环境 JDK 版本&#xff1a;需使用 JDK 17&#xff08;Nacos 3.x 已放弃对 JDK 8 的支持&#xff09;。Spri…

SPI-NRF24L01

模块介绍 NRF24L01是NORDIC公司生产的一款无线通信芯片&#xff0c;采用FSK调制&#xff0c;内部集成NORDIC自己的Enhanced Short Burst 协议&#xff0c;可以实现点对点或者1对6 的无线通信,通信速率最高可以达到2Mbps. NRF24L01采用SPI通信。 ①MOSI 主器件数据输出&#xf…

python黑科技:无痛修改第三方库源码

需求不符合 很多时候&#xff0c;我们下载的 第三方库 是不会有需求不满足的情况&#xff0c;但也有极少的情况&#xff0c;第三方库 没有兼顾到需求&#xff0c;导致开发者无法实现相关功能。 如何通过一些操作将 第三方库 源码进行修改&#xff0c;是我们将要遇到的一个难点…

一区严选!挑战5天一篇脂质体组学 DAY1-5

Day 1! 前期已经成功挑战了很多期NHANES啦&#xff01;打算来试试孟德尔随机化领域&#xff5e; 随着孟德尔随机化研究的普及&#xff0c;现在孟德尔发文的难度越来越高&#xff0c;简单的双样本想被接收更是难上加难&#xff0c;那么如何破除这个困境&#xff0c;这次我打算…

自学-408-《计算机网络》(总结速览)

文章目录 第一章 计算机网络概述1. 计算机网络的定义2. 计算机网络的基本功能3. 计算机网络的分类4. 计算机网络的层次结构5. 计算机网络的协议6. 计算机网络的组成部分7. 计算机网络的应用8. 互联网的概念 物理层的主要功能第二章 数据链路层和局域网1. 数据链路层的功能2. 局…

【质量管理】纠正、纠正措施和预防的区别与解决问题的四重境界

“质量的定义就是符合要求”&#xff0c;我们在文章【质量管理】人们对于质量的五个错误观念-CSDN博客中提到过&#xff0c;这也是质量大师克劳士比所说的。“质量的系统就是预防”&#xff0c;防止出现产品不良而造成的质量损失。 质量问题的解决可以从微观和宏观两个方面来考…

新手SEO优化实战快速入门

内容概要 对于SEO新手而言&#xff0c;系统化掌握基础逻辑与实操路径是快速入门的关键。本指南以站内优化为切入点&#xff0c;从网站结构、URL设计到内链布局&#xff0c;逐层拆解搜索引擎友好的技术框架&#xff1b;同时聚焦关键词挖掘与内容策略&#xff0c;结合工具使用与…

sqli-labs靶场 less 11

文章目录 sqli-labs靶场less 11 POS联合注入 sqli-labs靶场 每道题都从以下模板讲解&#xff0c;并且每个步骤都有图片&#xff0c;清晰明了&#xff0c;便于复盘。 sql注入的基本步骤 注入点注入类型 字符型&#xff1a;判断闭合方式 &#xff08;‘、"、’、“”&…

tomcat部署项目打开是404?

问题描述 今天在帮助一个小伙伴解决问题的时候 部署成功了 就是打不开总是404 他这个项目是公司的一个18年的项目 巨老&#xff01;&#xff01;&#xff01; HTTP状态 404 - 未找到 类型 状态报告 描述 源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示…

[Linux]解决虚拟机 ubantu系统下网络的问题

问题来源&#xff1a;打开ubantu发现网络连接标识消失 解决步骤&#xff1a; 重新安装&#xff0c;前面操作无效 切换桥接模式、直连手机网络 已解决&#xff1a;

如何使用stable diffusion 3获得最佳效果

参考&#xff1a;How to get the best results from Stable Diffusion 3 Scaling Rectified Flow Transformers for High-Resolution Image Synthesis prompting SD3 不再受限于CLIP的最长77个token的长度限制&#xff0c;可以输入更长的prompt。 &#xff08;两个CLIP模型的…

Java学习笔记1——编程基础

一、整数类型变量 注意&#xff1a;每个字符型常量占两个字节 二、自动类型转换和强制类型转换 ​​​三、算术运算符 四、赋值运算符 五、比较运算符 六、逻辑运算符 七、运算符的优先级 运算符的优先级可以通过以下口诀来记忆&#xff1a; 括号优先&#xff0c;单目次之&am…

微服务核心知识点深度解析:从组件到架构设计

微服务核心知识点深度解析&#xff1a;从组件到架构设计 微服务核心知识点深度解析&#xff1a;从组件到架构设计一、Spring Cloud 5 大核心组件详解二、服务注册与发现&#xff1a;微服务的 “通讯录”概念解析Spring Cloud 中的实现 三、Nacos&#xff1a;不止是注册中心核心…

SpringBoot3+EasyExcel通过WriteHandler动态实现表头重命名

方案简介 为了通过 EasyExcel 实现动态表头重命名&#xff0c;可以封装一个方法&#xff0c;传入动态的新表头名称列表&#xff08;List<String>&#xff09;&#xff0c;并结合 WriteHandler 接口来重命名表头。同时&#xff0c;通过 EasyExcel 将数据直接写入到输出流…

Python小练习系列 Vol.11:回文数筛选(filter + 字符串反转)

&#x1f9e0; Python小练习系列 Vol.11&#xff1a;回文数筛选&#xff08;filter 字符串反转&#xff09; &#x1f50d; 本期我们用 Python 的 filter() 函数结合字符串反转技巧&#xff0c;一行代码搞定“回文数”的判断与筛选&#xff01; &#x1f9e9; 一、题目描述 回…

BUUCTF-web刷题篇(5)

13.upload1 文件上传漏洞&#xff08;上传图片&#xff09; 按照传统方法&#xff0c;新建文件&#xff08;xinjian&#xff09;写一句话木马&#xff0c;利用Windows文件后缀识别的特点&#xff0c;将后缀名改为图片后缀名(xinjian.jpg)&#xff0c;上传文件&#xff0c;抓包…

NestJS——创建项目、编写User模块

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

《Python Web部署应知应会》No2:如何基于FastAPI 和 OLLAMA 架构实现高并发 AI 推理服务

《Python Web部署应知应会》No2&#xff1a;如何基于FastAPI 和 OLLAMA 架构实现高并发 AI 推理服务&#xff08;上&#xff09; 摘要&#xff1a; 在 FastAPI 和 OLLAMA 架构中实现高并发 AI 推理服务&#xff0c;并优化性能指标采集和缓存策略&#xff0c;可以充分利用 asy…