线程池-手写线程池C++11版本(生产者-消费者模型)

本项目是基于C++11的线程池。使用了许多C++的新特性,包含不限于模板函数泛型编程、std::future、std::packaged_task、std::bind、std::forward完美转发、std::make_shared智能指针、decltype类型推断、std::unique_lock锁等C++11新特性功能。


本项目有一定的上手难度。推荐参考系列文章

C++11实用技术(一)auto与decltype的使用

C++11实用技术(二)std::function和bind绑定器

C++11实用技术(三)std::future、std::promise、std::packaged_task、async

C++ operator关键字的使用(重载运算符、仿函数、类型转换操作符)

右值引用以及move移动语义和forward 完美转发

C++标准库中的锁lock_guard、unique_lock、shared_lock、scoped_lock、recursive_mutex


代码结构

本项目线程池功能分以下几个函数去实现:

threadpool.init(isize_t num);设置线程的数量
threadpool::get(TaskFuncPtr& task);读取任务队列中的任务
threadpool::run();通过get()读取任务并执行
threadpool.start(); 启动线程池,并通过run()执行任务
threadpool.exec();封装任务到任务队列中
threadpool.waitForAllDone();等待所有任务执行完成
threadpool.stop();分离线程,释放内存

threadpool.init

init的功能是初始化线程池,主要是设置线程的数量到类的成员变量中。

bool ZERO_ThreadPool::init(size_t num)
{
	std::unique_lock<std::mutex> lock(mutex_);

	if (!threads_.empty())
	{
		return false;
	}

	threadNum_ = num;
	return true;
}

threadNum_:保存线程的数量,在init函数中被赋值

此处使用unique_lock或lock_guard的加锁方式都能实现自动加锁和解锁。但是unique_lock可以进行临时解锁和再上锁,而lock_guard不行,特殊情况下还是必须使用unique_lock(用到条件变量的情况)。(lock_guard比较简单,相对来说性能要好一点)

threadpool::get

从任务队列中获取获取任务,这里其实就是我们的消费者模块

bool ZERO_ThreadPool::get(TaskFuncPtr& task)
{
	std::unique_lock<std::mutex> lock(mutex_);
	if (tasks_.empty()) //判断任务是否存在
	{
		//要终止线程池   bTerminate_设置为true,任务队列不为空
		condition_.wait(lock, [this] { return bTerminate_ || !tasks_.empty(); });
	}
	if (bTerminate_)
		return false;
	if (!tasks_.empty())
	{
		task = std::move(tasks_.front());  // 使用了移动语义
		tasks_.pop(); //释放资源,释放一个任务
		return true;
	}
	return false;
}
  • 条件变量condition_.wait(lock, [this] { return bTerminate_ || !tasks_.empty(); });是需要一直等待条件完成才退出。即任务终止,或者任务队列不为空时,就会退出条件变量的阻塞状态,继续执行下面的逻辑。

  • task = std::move(tasks_.front()); 使用了移动语义,将 tasks_.front() 的内容移动到了 task 中。可以减少内容拷贝。移动完之后tasks_.front() 的内容会变为未指定的状态,所以直接pop掉就好了。

threadpool::run

这里是运行我们的任务部分。包括调用get在任务队列中获取任务,以及执行任务。

void ZERO_ThreadPool::run()  // 执行任务的线程
{
	//调用处理部分
	while (!isTerminate()) // 判断是不是要停止
	{
		TaskFuncPtr task;
		bool ok = get(task);        // 读取任务
		if (ok)
		{
			++atomic_;
			try
			{
				if (task->_expireTime != 0 && task->_expireTime < TNOWMS)
				{//如果设置了超时,并且超时了,就需要执行本逻辑
				//超时任务,本代码未实现,有需要可实现在此处
				}
				else
				{
					task->_func();  // 执行任务
				}
			}
			catch (...)
			{
			}
			--atomic_;
			}
		}
	}
}

atomic_:运行一个任务,该参数+1;执行完毕,该参数-1。这里是为了待会停止线程池时判断是否还有运行中的任务(未完成的线程)。

threadpool.start

创建线程,并把线程池存入vector中,后面释放线程池时,好一一释放线程。

bool ZERO_ThreadPool::start()
{
	std::unique_lock<std::mutex> lock(mutex_);
	if (!threads_.empty())
	{
		return false;
	}
	for (size_t i = 0; i < threadNum_; i++)
	{
		threads_.push_back(new thread(&ZERO_ThreadPool::run, this));
	}
	return true;
}

threads_.push_back(new thread(&ZERO_ThreadPool::run, this));创建线程,线程的回调函数为run。

threadpool.exec

exec是将我们的任务存入任务队列中,这段代码是本项目最难的,用了很多C++的新特性。

/*
	template <class F, class... Args>
	它是c++里新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数
	auto exec(F &&f, Args &&... args) -> std::future<decltype(f(args...))>
	std::future<decltype(f(args...))>:返回future,调用者可以通过future获取返回值
	返回值后置
	*/
	template <class F, class... Args>
	auto exec(int64_t timeoutMs, F&& f, Args&&... args) -> std::future<decltype(f(args...))>//接受一个超时时间 `timeoutMs`,一个可调用对象 `f` 和其它参数 `args...`,并返回一个 `std::future` 对象,该对象可以用于获取任务执行的结果。
	{
		int64_t expireTime = (timeoutMs == 0 ? 0 : TNOWMS + timeoutMs);  // 根据超时时间计算任务的过期时间 `expireTime`,如果超时时间为 0,则任务不会过期。
		//定义返回值类型
		using RetType = decltype(f(args...));  // 使用 `decltype` 推导出 `f(args...)` 的返回值类型,并将其定义为 `RetType`(这里的using和typedef功能一样,就是为一个类型起一个别名)。
		// 封装任务 使用 `std::packaged_task` 将可调用对象 `f` 和其它参数 `args...` 封装成一个可执行的函数,并将其存储在一个 `std::shared_ptr` 对象 `task` 中。
		auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));

		TaskFuncPtr fPtr = std::make_shared<TaskFunc>(expireTime);  // 封装任务指针,设置过期时间 创建一个 `TaskFunc` 对象,并将任务的过期时间 `expireTime` 传递给它。
		fPtr->_func = [task]() {  // 具体执行的函数 将封装好的任务函数存储在 `TaskFunc` 对象的 `_func` 成员中,该函数会在任务执行时被调用。
			(*task)();
		};

		std::unique_lock<std::mutex> lock(mutex_);
		tasks_.push(fPtr);              // 将任务插入任务队列中
		condition_.notify_one();        // 唤醒阻塞的线程,可以考虑只有任务队列为空的情况再去notify

		return task->get_future();; //返回一个 `std::future` 对象,该对象可以用于获取任务执行的结果。
	}

使用了可变参数模板函数。
tasks_:保存任务的队列
condition_.notify_one():保存一个任务唤醒一个条件变量
std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
std::bind:将参数列表和函数绑定,生成一个新的可调用对象
std::packaged_task:将任务和feature绑定在一起的模板,是一种封装对任务的封装。

本函数用到了泛型编程模板函数,输入参数有3个:一个超时时间 timeoutMs,一个可调用对象 f 和参数 args...。采用返回值后置的方式返回一个std::future对象。这里采用返回值后置是为了方便使用decltype(f(args…)推导数据类型。

auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward(f), std::forward(args)…));是将我们传进来的任务函数和参数bind成一个对象,这个对象可以看作是一整个函数,其返回值就是RetType 类型,并且没有输入参数。所以用std::packaged_task<RetType()>这样的格式来打包封装。封装好的对象用智能指针(std::make_shared)来管理。

同时还要创建一个TaskFunc的对象,同样用智能指针管理,这个对象包括两项内容,一个就是超时时间,一个就是我们封装好的task对象。
通过TaskFuncPtr fPtr = std::make_shared(expireTime);和fPtr->_func = task {(*task)();};两条代码将这两项传进去。

最后会通过task->get_future()返回我们任务函数执行的结果返回值。

threadpool.waitForAllDone

等待所有任务执行完成。

bool ZERO_ThreadPool::waitForAllDone(int millsecond)
{
	std::unique_lock<std::mutex> lock(mutex_);
	if (tasks_.empty() && atomic_ == 0)
		return true;
	if (millsecond < 0)
	{
		condition_.wait(lock, [this] { return tasks_.empty() && atomic_ == 0; });
		return true;
	}
	else
	{
		return condition_.wait_for(lock, std::chrono::milliseconds(millsecond), [this] { return tasks_.empty() && atomic_ == 0; });
	}
}

使用条件变量来等待任务执行完成。支持超时执行功能。

此处unique_lock的使用是必须的: 条件变量condition_在wait时会进行unlock再进入休眠, lock_guard并无该操作接口

threadpool.stop

终止线程池。会调用waitForAllDone等待所有任务执行完成再终止。

void ZERO_ThreadPool::stop()
{
	{
		std::unique_lock<std::mutex> lock(mutex_);
		bTerminate_ = true;
		condition_.notify_all();
	}
	waitForAllDone();
	for (size_t i = 0; i < threads_.size(); i++)
	{
		if (threads_[i]->joinable())
		{
			threads_[i]->join();
		}
		delete threads_[i];
		threads_[i] = NULL;
	}
	std::unique_lock<std::mutex> lock(mutex_);
	threads_.clear();
}

通过join等线程执行完成后才返回。

主函数调用

class Test
{
public:
	int test(int i) {
		cout << _name << ", i = " << i << endl;
		Sleep(1000);
		return i;
	}
	void setName(string name) {
		_name = name;
	}
	string _name;
};
void test3() // 测试类对象函数的绑定
{
	ZERO_ThreadPool threadpool;
	threadpool.init(2);
	threadpool.start(); // 启动线程池
	Test t1;
	Test t2;
	t1.setName("Test1");
	t2.setName("Test2");
	auto f1 = threadpool.exec(std::bind(&Test::test, &t1, std::placeholders::_1), 10);
	auto f2 = threadpool.exec(std::bind(&Test::test, &t2, std::placeholders::_1), 20);
	cout << "t1 " << f1.get() << endl;
	cout << "t2 " << f2.get() << endl;
	threadpool.stop();
}
int main()
{
	test3(); // 测试类对象函数的绑定
	cout << "main finish!" << endl;
	return 0;
}

运行结果:
在这里插入图片描述

本项目完整代码下载地址基于C++11的线程池

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

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

相关文章

【Linux升级之路】5_基础IO

&#x1f31f;hello&#xff0c;各位读者大大们你们好呀&#x1f31f; &#x1f36d;&#x1f36d;系列专栏&#xff1a;【Linux升级之路】 ✒️✒️本篇内容&#xff1a;文件操作&#xff0c;文件管理&#xff0c;重定向&#xff0c;简易shell添加重定向功能&#xff0c;文件属…

人物启示-张一鸣与陆奇

在科技行业中&#xff0c;张一鸣与陆奇可谓是两位颇具影响力的人物。张一鸣和陆奇分别是字节跳动&#xff08;TikTok 的母公司&#xff09;的创始人和百度前总裁。张一鸣作为字节跳动的创始人&#xff0c;成功打造了今日头条、抖音等知名产品&#xff0c;而陆奇则曾任微软副总裁…

Django Rest_Framework(二)

文章目录 1. http请求响应1.1. 请求与响应1.1.1 Request1.1.1.1 常用属性1&#xff09;.data2&#xff09;.query_params3&#xff09;request._request 基本使用 1.1.2 Response1.1.2.1 构造方式1.1.2.2 response对象的属性1&#xff09;.data2&#xff09;.status_code3&…

4G型无线液位变送器是什么?

4G型无线液位变送器采用了四代无线通讯技术&#xff0c;与普通液位计相比&#xff0c;免去了布线的烦恼&#xff0c;无需时刻监控现场&#xff0c;在大幅提高工作效率和减少人力成本的同时&#xff0c;还可以随时随地获取监测数据。 4G型无线液位变送器的功能优势&#xff1a;…

jmeter创建一个压测项目

1.jemeter新建一个项目&#xff1a; 2.接下来对Thread进行描述&#xff0c;也可以先使用默认的Thread进行操作。 3.添加http请求头的信息。按照如图所示操作 4.在请求头里面添加必要的字段&#xff0c;可以只填必要字段就可以 5.添加Http请求信息&#xff0c;如下图&#xff…

第三章 图论 No.4最小生成树的简单应用

文章目录 裸题&#xff1a;1140. 最短网络裸题&#xff1a;1141. 局域网裸题&#xff1a;1142. 繁忙的都市裸题&#xff1a;1143. 联络员有些麻烦的裸题&#xff1a;1144. 连接格点 存在边权为负的情况下&#xff0c;无法求最小生成树 裸题&#xff1a;1140. 最短网络 1140. 最…

八大排序

目录 选择排序-直接插入排序 插入排序-希尔排序 选择排序-简单选择排序 选择排序-堆排序 交换排序-冒泡排序 交换排序-快速排序 归并排序 基数排序 选择排序-直接插入排序 基本思想: 如果碰见一个和插入元素相等的&#xff0c;那么插入元素把想插入的元素放在相等元素…

Golang空结构体struct{}的作用是什么?

文章目录 占位符&#xff1a;通道标识&#xff1a;键集合&#xff1a;内存占用优化&#xff1a;总结&#xff1a; 在Go语言中&#xff0c;空结构体 struct{}是一种特殊的数据类型&#xff0c;它不占用任何内存空间。空结构体没有任何字段&#xff0c;也没有任何方法。尽管它看起…

使用vue模拟通讯录列表,对中文名拼音首字母提取并排序

一个功能需求,做一个类似联系人列表的功能,点击名称获取对应的id,样式简陋,只是一个模板,原来是uniapp项目,根据需要改成了vue,需要的自行设计css&#xff08;万是有一个mo的音&#xff09; 流程 获取数据提取首个字的拼音的首个字母排序并分组 上代码&#xff1a; <temp…

基于OFDM通信系统的低复杂度的资源分配算法matlab性能仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 .......................................................................%子载波分配[~,po…

使用MyBatis操作数据库

hi,大家好,今天为大家带来MyBatis操作数据库的知识 文章目录 &#x1f437;1.根据MyBatis操作数据库&#x1f9ca;1.1查询操作&#x1f347;1.1.1无参查询&#x1f347;1.1.2有参查询 &#x1f9ca;1.2删除操作&#x1f9ca;1.3修改操作&#x1f9ca;1.4增加操作&#x1f9ca;…

【MySQL】MySQL数据类型

文章目录 一、数据类型的分类二、tinyint类型2.1 创建有符号数值2.2 创建无符号数值 三、bit类型三、浮点类型3.1 float3.2 decimal类型 四、字符串类型4.1 char类型4.2 varchar类型 五、日期和时间类型六、枚举和集合类型6.1 enum的枚举值和set的位图结构6.2 查询集合find_in_…

【C++】C++回调函数基本用法(详细讲解)

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

MySQL: Failed to Connect to MySQL at XXXX:3306 with user root

客户端连接MySQL服务器&#xff0c;报错&#xff1a; 解决方案&#xff1a; 没有让root用户远程登录&#xff0c;需要设置&#xff1b; 进入MySQL服务器&#xff0c;修改一下 # mysql -h localhost -uroot -P3306 -p12345678 mysql: [Warning] Using a password on the comm…

基于Java的新闻全文搜索引擎的设计与实现

中文摘要 本文以学术研究为目的&#xff0c;针对新闻行业迫切需求和全文搜索引擎技术的优越性&#xff0c;设计并实现了一个针对新闻领域的全文搜索引擎。该搜索引擎通过Scrapy网络爬虫工具获取新闻页面&#xff0c;将新闻内容存储在分布式存储系统HBase中&#xff0c;并利用倒…

【UE4 RTS】01-Camera SetUp

UE版本&#xff1a;4.24.3 前言 本篇主要完成游戏模式、玩家控制器和玩家控制的Pawn的设置&#xff0c;下一篇介绍如何实现Pawn的移动 步骤 1. 首先创建一个俯视角游戏模板 2. 首先删除“TopDownCharacter”&#xff0c; 3. 新建一个文件夹命名为“RTS_Toturial” 在文件夹…

adb 命令行执行单元测试

文章目录 1、配置 adb 环境变量2、adb 执行测试3、官方文档解读 adb 使用&#xff08;1&#xff09;第一条执行测试的adb命令&#xff08;2&#xff09;am instrument 参数&#xff08;3&#xff09;-e 参数 的 key-value键值对&#xff08;4&#xff09;用法用例 4、存在问题 …

SpringBoot系列---【三种启动传参方式的区别】

三种启动传参方式的区别 1.三种方式分别是什么? idea中经常看到下面三种启动传参方式 优先级 Program arguments > VM options > Environment variable > 系统默认值 2.参数说明 2.1、VM options VM options其实就是我们在程序中需要的运行时环境变量&#xff0c;它需…

ArcGIS Pro基础入门、制图、空间分析、影像分析、三维建模、空间统计分析与建模、python融合、案例应用全流程科研能力提升

目录 第一章 入门篇 GIS理论及ArcGIS Pro基础 第二章 基础篇 ArcGIS数据管理与转换 第三章 数据编辑与查询、拓扑检查 第四章 制图篇 地图符号与版面设计 第五章 空间分析篇 ArcGIS矢量空间分析及应用 第六章 ArcGIS栅格空间分析及应用 第七章 影像篇 遥感影像处理 第八…

激活函数总结(二):ELU、SELU、GELU激活函数

激活函数总结&#xff08;二&#xff09;&#xff1a;ELU、SELU、GELU激活函数 1 引言2. 激活函数2.1 ELU&#xff08;Exponential Linear Unit&#xff09;激活函数2.2 SELU&#xff08;Scaled Exponential Linear Unit&#xff09;激活函数2.3 GELU激活函数 3. 总结 1 引言 …