【C++】线程池学习记录

【C++】线程池学习记录

前言

在多线程编程中,线程的创建和销毁是开销较大的操作,特别是在需要频繁执行任务的情况下。由此引入了线程池的思想:在程序执行之初就创建一定数量的线程,并将他们保存在线程池中;当需要执行任务时,从线程池中获取空闲线程分配任务执行;执行完毕后将线程返回到线程池可以被其他任务复用。

线程池的关键部分包括:

  1. 线程池管理器:负责创建、管理和控制线程池。它负责线程的创建、销毁和管理,以及线程池的状态监控和调度任务。
  2. 工作队列:用于存储待执行的任务。当线程池中的线程都在执行任务时,新的任务会被放入工作队列中等待执行。
  3. 线程池线程:实际执行任务的线程。线程池中会维护一组线程,这些线程可以被重复使用,从而避免了频繁创建和销毁线程的开销。

线程池的运行机制

  1. 当任务到达时,线程池管理器会检查线程池中是否有空闲的线程。如果有,则将任务分配给空闲线程执行;如果没有,则进入下一步。
  2. 如果线程池中的线程数量未达到最大限制,线程池管理器会创建一个新的线程,并将任务分配给该线程执行。
  3. 如果线程池中的线程数量已达到最大限制,并且工作队列未满,则将任务放入工作队列中等待执行。
  4. 当线程池中的线程执行完任务后,会从工作队列中获取下一个任务并执行。

本文中将使用C++实现两种机制的线程池(Fixed模式和Cached模式)

设计总览:
在这里插入图片描述
最终使用线程池的时候通过实例化类对象的形式,那么线程池的开发即为.h头文件
整个线程池最终需要抽象出以下类

  1. 线程池类
class ThreadPool {

public:
	//线程池构造函数
	ThreadPool()
	
	//线程池析构函数函数
	~ThreadPool()
	
	//设置Task任务队列数量
	void set_taskQueMaxThreshold(int threshold)

	//设置线程池Cache模式下最大线程数量
	void setThreadSizeThreshold(int threshold)
	
	//给线程池提交任务
	template<typename Func,typename... Args>
	auto submitTask(Func&& func, Args&&...args) -> std::future<decltype(func(args...))>
	
	//设置线程池模式
	void setMode(PoolMode mode)
	
	//开启线程池;
	void start(int initThreadSize = std::thread::hardware_concurrency())//hardware_concurrency()系统核心数量
	
	//禁止线程池进行拷贝构造和赋值
	ThreadPool(const ThreadPool&) = delete;
	ThreadPool& operator= (const ThreadPool&) = delete;
private:

	//线程函数
	void threadFunc(int threadid)

	//检查pool的运行状态
	bool checkRunningState()const
private:
	std::unordered_map<int, std::unique_ptr<Thread>> threads_;

	int initThreadSize_;//初始的线程数量
	int threadSizeThreshold_;//线程上限数量
	std::atomic_int curThreadSize_;//当前线程池线程的数量
	std::atomic_int idleThreadSize_;//记录线程空闲时间的数量
	
	using Task = std::function<void()>;
	std::queue<Task> taskQue_;//任务队列
	std::atomic_uint taskSize_; //任务数量
	int taskQueMaxThreshold_;//任务对象数量上限阈值

	std::mutex taskQueMtx_;//保证任务队列的线程安全
	std::condition_variable  notFull_;//保证任务队列不满
	std::condition_variable notEmpty_;//保证任务队列不空
	std::condition_variable exitCond_;//等到线程资源全部回收

	PoolMode poolMode_;//当前线程池的工作模式
	std::atomic_bool isPoolRunning_;//当前线程池的启动状态
};
  1. 线程类
class Thread {
	//线程类型
public:
	//定义一个线程函数对象类型
	using ThreadFunc = std::function<void(int)>;
	//构造函数
	Thread(ThreadFunc func)
	//析构函数
	~Thread() = default;
	//启动线程
	void start()
	//获取线程ID
	int getThreadId() const

private:
	ThreadFunc func_;
	static int generateId_;
	int threadId_;//保存线程ID

};
  1. 线程池模式类
enum class PoolMode {  //enum 是一种用来定义枚举类型的关键字。枚举类型允许您定义一组命名的整数常量,这些常量在枚举类型的范围内具有唯一的标识符。
	MODE_FIXED,
	MODE_CACHED,
};

下面开始分块介绍函数实现以及相关技术点

从提交任务开始讲吧
线程池的使用是通过实例化ThreadPool对象,调用start函数开启线程池;然后通过pool对象的submitTask函数提交任务进行处理。由于任务的类型可能是多种多样的,可能具有返回值也可能没有,可能具有多个参数,也可能没有参数;那么submit函数的实现要考虑到任务的类型、返回值和参数三个因素。

  • 第一个是任务的类型可以通过函数模板实现
  • 第二个函数的返回值类型不明,所以返回的可以是一个auto类型的对象,这个对象的类型可以通过future类实现,其类型通过类型推导实现
  • 第三个参数输入需要用到引用折叠原理实现

这里是确定了submit函数最终实现需要达到的效果,那么接着说线程池基本功能的实现。
整个线程池实现过程中实现了两种基本模式
Fixed: 第一个是固定线程池,也就是说在程序启动时创建固定数量的线程,当任务提交给线程池之后,进入任务队列,如果线程池里有空闲线程,那么就取出来空闲线程处理任务,如果没有空闲线程任务就要在任务队列进行阻塞等待,直到任务执行完毕再继续处理,最后到执行完毕。
Cached: 第二个是动态增加的线程池,在程序启动之初先在线程池中创建一部分线程,当任务数量大于此时的线程数量,那么线程池会再次创建一部分线程执行任务,在任务执行完之后,将线程回收,使线程数量仍然保持预设数量。
从以上流程描述可以总结出所需要的技术要点:

  • 需要使用map和vector管理线程对象和任务对象
  • 需要使用condition_variable和互斥锁mutex来实现任务提交线程和任务执行线程之间的通信机制
  • 需要使用可变参的模板编程和引用折叠来实现submitTask接口
  • 需要使用future对象来定制返回值

下面从任务提交开始函数执行的顺序来讲解函数实现;

  • 创建线程池
ThreadPool pool;
pool.start(4);

start函数需要设置初始线程数量,以及线程池的一些默认状态

void start(int initThreadSize = std::thread::hardware_concurrency())//hardware_concurrency()系统核心数量
	{
		//设置线程池的启动状态
		isPoolRunning_ = true;
		initThreadSize_ = initThreadSize;
		curThreadSize_ = initThreadSize;
		//创建线程对象
		for (int i = 0; i < initThreadSize_; i++) {
			//创建thread线程对象的时候把线程函数给到thread线程对象
			//把线程函数绑定到Thread对象,然后拿到ThreadPool的对象指针this
			auto ptr = new Thread(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));
			//threads_.emplace_back(std::move(ptr));//与 push_back() 函数类似,但是 emplace_back() 可以接受任意数量的参数,并将它们传递给元素类型的构造函数,从而在原地构造元素,而不需要额外的拷贝或移动操作。
			int threadId = ptr->getThreadId();
			threads_.emplace(threadId, std::move(ptr));
		}
		//启动所有线程 std::vector<Thread*> thread_
		for (int i = 0; i < initThreadSize_; i++) {
			threads_[i]->start();//需要去执行一个线程函数
			idleThreadSize_++;//记录初始空闲线程数量
		}
	}

重点关注此句代码

auto ptr = new Thread(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));

此句中用指针指向一个新的线程对象,线程对象中使用绑定器bind绑定了一个线程函数,线程函数中this指针表示将当前对象的指针多为threadFunc函数的隐式参数传递,std::placeholders::_1表示占位符,表示ThreadPool::threadFunc方法接受的第一个参数
紧接着介绍一下 ThreadPool::threadFunc 函数

void threadFunc(int threadid)
	{
		auto lastDoTime = std::chrono::high_resolution_clock().now();
		for (;;)//while (isPoolRunning_)
		{
			Task task;//默认智能指针初始化为空
			//创建一个局部作用域,在拿走任务后就把锁释放掉(自动析构)
			{
				std::cout << "尝试获取任务!" << "tid:" << std::this_thread::get_id() << std::endl;
				//先获取锁
				std::unique_lock<std::mutex> lock(taskQueMtx_);
				//cache模式下,可能创建了很多线程,但是空闲时间超过60s,应该把多余的线程结束回收?
					//结束前回收(超过initThreadSize_数量的线程要回收掉)
					//当前时间-上一次线程执行时间 > 60S
				//锁+双重判断
				while (taskQue_.size() == 0)
				{
					线程池要结束,回收线程资源
					if (!isPoolRunning_) {
						//threadid==>thread对象==>删除
						threads_.erase(threadid);
						exitCond_.notify_all();
						std::cout << "thread" << std::this_thread::get_id() << "撤销!" << std::endl;
						return;
					}

					if (poolMode_ == PoolMode::MODE_CACHED)
					{
						//每一秒钟返回一次,怎么区分超时返回还是有任务待返回
							//条件变量,超时返回了
						if (std::cv_status::timeout == notEmpty_.wait_for(lock, std::chrono::seconds(1)))
						{
							auto now = std::chrono::high_resolution_clock().now();
							auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastDoTime);
							if (dur.count() >= THREAD_MAX_IDLE_TIME
								&& curThreadSize_ > initThreadSize_)
							{
								//回收线程
								//记录线程相关数量的值--
								//线程列表中的线程对象删除(难点是没办法确定threadFunc 绑定的是哪个线程对象)
								threads_.erase(threadid);
								curThreadSize_--;
								idleThreadSize_--;
								std::cout << "thread" << std::this_thread::get_id() << "撤销!" << std::endl;
								return;
							}
						}
					}
					else
					{
						//等待notEmpty条件,任务队列不为空
						notEmpty_.wait(lock);
					}
				}
				//线程被分配任务,数量++
				idleThreadSize_--;
				//从任务队列中取一个任务出来
				//std::shared_ptr<Task> task = taskQue_.front();
				task = taskQue_.front();
				//任务--
				taskQue_.pop();
				taskSize_--;
				//通知生产任务
				//如果依然有剩余任务,继续通知其他线程执行任务
				if (taskQue_.size() > 0)
				{
					notEmpty_.notify_all();
				}
				//拿出任务进行通知
				notFull_.notify_all();
			}
			std::cout << "获取任务成功!" << "tid:" << std::this_thread::get_id() << std::endl;
			//当前线程负责执行这个任务
			if (task != nullptr)
			{
				task();//执行function<void()>
			}
			//线程处理完任务,数量++
			idleThreadSize_++;
			lastDoTime = std::chrono::high_resolution_clock().now();//更新线程执行完任务的时间
		}
	}

threadFunc函数用于从任务队列中获取任务并执行,
首先使用了for(;;)函数不断地从任务队列中获取任务并执行;获取任务时创建了一个局部作用域用于智能指针自动析构;在获取任务时,首先创建一个智能锁用于保护获取任务时的线程安全,当任务队列为空的时候,进入等待,直到有线程进入开始执行

如果线程池为结束状态,则删除线程池中所有线程,并通知条件变量通知其他等待中的线程,停止执行

如果为Cached模式,那么执行时间判断,如果等待任务的时间超过阈值,则进一步检查当前线程的空闲时间是否超过了设定的最大空闲时间,如果超过了最大空闲时间,并且线程数量大于起始设定数量则会回收线程资源

然后定义了Task对象用于从存储任务队列中获取任务,获取成功之后执行,这里创建的Task是一个函数对象,将任务队列中取出的任务交由Task进行处理。

using Task = std::function<void()>;

紧接着开始使用pool.submitTask()提交任务,具体实现如下:

auto submitTask(Func&& func, Args&&...args) -> std::future<decltype(func(args...))>
	{
		//打包任务,放到任务队列里
		using RType = decltype(func(args...));
		auto task = std::make_shared<std::packaged_task<RType()>>(
			std::bind(std::forward<Func>(func), std::forward<Args>(args)...));
		std::future<RType> Result = task->get_future();

		//获取锁
		std::unique_lock<std::mutex> lock(taskQueMtx_);

		//用户提交任务,最长不能阻塞超过1s,否则判断提交任务失败
		if (!notFull_.wait_for(lock, std::chrono::seconds(1)
			, [&]()->bool {return taskQue_.size() < (size_t)taskQueMaxThreshold_; }))
		{
			//表示等待一秒钟条件依然未完成,
			std::cerr << "Task queue is full, submit task fail!" << std::endl;
			auto task = std::make_shared<std::packaged_task<RType()>>(
				[]()->RType {return RType(); });
			return task->get_future();
		}
		//如果有空余,把任务放入任务队列中
		//taskQue_.emplace(sp);
		//using Task = std::function<void()>;
		//此时Task对象为空对象,使用lambda表达式包装中间层
		taskQue_.emplace([task]() {(*task)(); });

		taskSize_++;
		//新放了任务,任务队列不空了,此时再notEmpty_进行通知
		notEmpty_.notify_all();

		//Cache模式(场景:任务处理比较紧急,小而快的任务)需要根据任务数量和空闲线程的数量,判断是否需要创建新的线程
		if (poolMode_ == PoolMode::MODE_CACHED
			&& taskSize_ > idleThreadSize_
			&& curThreadSize_ < threadSizeThreshold_)
		{
			std::cout << "new thread" << std::this_thread::get_id() << "<<<<<<<<<<" << std::endl;
			//创建新线程
			auto ptr = new Thread(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));
			//threads_.emplace_back(std::move(ptr));//与 push_back() 函数类似,但是 emplace_back() 可以接受任意数量的参数,并将它们传递给元素类型的构造函数,从而在原地构造元素,而不需要额外的拷贝或移动操作。
			int tId = ptr->getThreadId();
			threads_.emplace(tId, std::move(ptr));
			threads_[tId]->start();//启动线程
			//修改线程相关变量
			idleThreadSize_++;
			curThreadSize_++;
		}
		//返回任务的Result对象
		return Result;
	}

由于不知道具体执行的任务函数的参数和返回值,所以第一步先确定函数的返回类型,这里使用了using RType = decltype(func(args...));自动推导函数表达式的数据类型;

第二步定义std::packaged_task包装任务函数,并通过bind()函数绑定传入的不确定个数的参数,使用std::forward完美转发函数将参数以原始类型(左值或者右值引用)转发给另一个函数以保持函数的原始类型,确保不会产生不必要的拷贝或者移动操作;

auto task = std::make_shared<std::packaged_task<RType()>>(
			std::bind(std::forward<Func>(func), std::forward<Args>(args)...));

第三步使用std::future对象表示将来会产生结果的异步任务,用于获取任务的执行结果

std::future<RType> Result = task->get_future();

第四步提交任务队列,首先锁定任务队列,保证对任务队列的操作是线程安全的,然后判断任务队列是否已满,如果等待时间超过阈值,则表示任务提交失败,并返回空Future对象
如果任务提交成功,则将任务放入任务队列,并通知等待中的线程可以继续执行了,任务队列不为空了

第五步当线程池处于Cached模式时,则进行判断,如果任务数量大于空闲线程数量,并且当前线程数量未超过阈值,则创建新线程,创建线程后修改线程相关变量,然后重新启动线程

最后任务只从结束后返回任务执行的结果

注意以上提交任务时taskQue_.emplace([task]() {(*task)(); });,这里taskQue_使用队列创建,其成员函数emplace()用于在队列的末尾插入一个元素,与push()不同,它允许直接构造元素插入,而不是创建对象再通过拷贝或者移动加入队列。同时在这里使用了Lambda表达式[task]() {(*task)(); },这里以值传递的方式捕获了外部的task变量,也就是上文中使用bind()函数绑定的任务对象,Lambda函数的函数体调用了被捕获的task变量,所以(*task)会调用std::packaged_task中包装的任务函数bind(),执行包装函数,然后将绑定后的函数加入到任务队列中。
到这里基本上实现了提交任务到任务队列以及创建线程池并执行的主要功能。以下是细节的介绍:
线程池支持的模式使用了创建枚举类
enum 是一种用来定义枚举类型的关键字。枚举类型允许您定义一组命名的整数常量,这些常量在枚举类型的范围内具有唯一的标识符。

enum class PoolMode { 
	MODE_FIXED,
	MODE_CACHED,
};

线程池的构造函数

	ThreadPool()
		:initThreadSize_(0)
		, threadSizeThreshold_(THREAD_MAX_SIZE)
		, idleThreadSize_(0)
		, curThreadSize_(0)
		, taskSize_(0)
		, taskQueMaxThreshold_(TASK_MAX_THRESHOLD)
		, poolMode_(PoolMode::MODE_FIXED)
		, isPoolRunning_(false)
	{}

线程池析构函数

~ThreadPool()
	{
		isPoolRunning_ = false;
		std::cout << "**********" << curThreadSize_ << " " << threads_.size() << "**********" << std::endl;
		//等待所有线程结束返回(两种状态:1.正在执行,2.阻塞)
		std::unique_lock<std::mutex> lock(taskQueMtx_);

		notEmpty_.notify_all();
		exitCond_.wait(lock, [&]()->bool {return threads_.size() == 0; });
	}

析构函数要尤其注意,等待线程池里的线程返回有两种状态,一个是阻塞,一个是正在执行中,这里进行析构的策略是等待线程队列为空,也就是全部执行完然后开始析构。

启动线程实现

void Thread::start() {
	//创建一个线程来执行线程函数
	std::thread t(func_, threadId_);
	t.detach();//分离线程
}

线程的构造和析构

	//构造函数
	Thread(ThreadFunc func)
		:func_(func)
		, threadId_(generateId_++)//使用成员变量接受线程函数,成员初始化列表来初始化类的成员变量 func_,将参数 func 的值传递给成员变量 func_。
	{}
	//析构函数
	~Thread() = default;

问题1:当线程池出作用域结束后,此时任务队列中还有任务,那么此时是等任务执行完再结束,还是不执行剩下的任务直接结束?

  • 26
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++线程池可以用来处理一些需要并发执行的任务,同时避免频繁创建和销毁线程所带来的开销。下面是一个简单的C++线程池实现: ```cpp #include <iostream> #include <queue> #include <thread> #include <mutex> #include <condition_variable> class ThreadPool { public: ThreadPool(size_t num_threads) { for (size_t i = 0; i < num_threads; ++i) { threads_.emplace_back([this] { while (true) { Task task; { std::unique_lock<std::mutex> lock(mutex_); cond_.wait(lock, [this] { return !tasks_.empty() || stop_; }); if (stop_ && tasks_.empty()) return; task = std::move(tasks_.front()); tasks_.pop(); } task(); } }); } } ~ThreadPool() { { std::unique_lock<std::mutex> lock(mutex_); stop_ = true; } cond_.notify_all(); for (auto& thread : threads_) { thread.join(); } } template <typename Func, typename... Args> void AddTask(Func&& func, Args&&... args) { auto task = std::bind(std::forward<Func>(func), std::forward<Args>(args)...); { std::unique_lock<std::mutex> lock(mutex_); tasks_.emplace(std::move(task)); } cond_.notify_one(); } private: using Task = std::function<void()>; std::vector<std::thread> threads_; std::queue<Task> tasks_; std::mutex mutex_; std::condition_variable cond_; bool stop_ = false; }; ``` 这个实现定义了一个ThreadPool类,构造函数中创建了指定数量的线程,并且每个线程都会从任务队列中获取任务并执行;析构函数中会通知所有线程停止执行,并等待所有线程退出;AddTask方法用于添加一个任务到任务队列中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值