线程池的概念
在学习使用stl容器的时候,我们见到了stl容器使用了内存池来进行时间优化,因为用户在使用new、malloc等向系统申请空间的时候,身份会发生变化,同时有可能要执行操作系统内的内存处理算法,这可能会比较耗时。于是使用了内存池来提前申请好大块内存空间,需要用户自行进行管理。与内存池类似,提前准备好的线程,用来随时处理任务,这就被称作线程池。
线程池是线程的一种使用模式。线程过多会带来调度开销,从而影响缓存局部性和整体性能。而线程池中维护着多个线程,等待分配可并发执行的任务。这就避免了在处理短时间任务时创建和销毁线程的代价。线程池不仅能保证内核的充分利用,还能防止过度调度。可用线程数量取决于可用的并发处理器、处理器内核、内网络sockets等的数量。
线程池的应用场景
- 需要大量的线程来完成任务,并且完成任务的时间比较短。WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,任务数量巨大。但是对于长时间的任务,比如一共Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池的情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,可能会出现错误。
线程池的简单实现
线程池的实现可以先使用一个命名空间来封装这个类。线程池中类的成员变量有:num_代表该线程池中有多少个线程。task_queue_代表一个队列,该成员是一个临界资源,用来存放任务。还有一把锁和一个条件变量。
#include<iostream>
#include<string>
#include<queue>
#include<unistd.h>
namespace ns_threadpool
{
const int g_num = 5;
template<class T>
class ThreadPool
{
private:
int num_;
std::queue<T> task_queue_;//该成员是一个临界资源
pthread_mutex_t mtx_;
pthread_cond_t cond_;
};
} // namespace ns_threadpool
线程池中的构造和析构函数:主要是对锁和条件变量进行初始化和销毁操作,并对具体创建线程的数量进行初始化
ThreadPool(int num = g_num)
:num_(num)
{
pthread_mutex_init(&mtx_,nullptr);
pthread_cond_init(&cond_,nullptr);
}
~ThreadPool()
{
pthread_mutex_destroy(&mtx_);
pthread_cond_destroy(&cond_);
}
初始化线程池:初始化线程池即创建num_数量的线程,这里只需要创建出来即可,不需要等待等操作。需要注意的是这里需要传入this指针,具体原因后面介绍Routine函数时进行介绍
void InitThreadPool()
{
pthread_t tid;
for(int i=0;i<num_;i++)
{
pthread_create(&tid,nullptr,Routine,(void*)this);
}
}
Routine线程执行函数:该函数由于是在类里面,所以如果不加上static来进行修饰,则会自带一个隐藏的this指针,这就和创建线程所需要的函数不符合,因此需要加上static。但是加上static后就不能访问类中的内容,因此之前在创建线程的时候刚好可以将this指针传入。
在该函数的实现方面可以先将线程进行分离再进行后续的处理工作。
为了线程安全还是需要对临界资源的访问进行加锁操作。使用条件变量来控制判断此时是否有任务需要执行,如果没有任务需要执行则等待,有则进行任务处理工作。
static void* Routine(void* args)
{
pthread_detach(pthread_self());
ThreadPool<T>* tp = (ThreadPool<T>*)args;