什么是线程池
一种线程的使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
什么地方可以用到线程池
- 任务短,但数量大,例如:网页请求这样的任务,使用线程池非常合适
- 对性能要求苛刻,要求服务器迅速响应客户请求
- 接受突发性的大量请求。
线程池实现
线程完成的任务
由一个队列来保存每个线程需要完成的任务
每个线程的要完成的任务都由一个类来表示
在此处,我们要线程任务就是通过Handler函数来处理数据
typedef void(*Handler_t)(int);
class ThreadTask
{
public:
ThreadTask()
{
Data_ = -1;
Handler_ = NULL;
}
ThreadTask(int Data,Handler_t Handler)
{
Data_=Data;
Handler_ = Handler;
}
void Run()
{
Handler_(Data_);
}
private:
int Data_;
Handler_t Handler_;
};
线程池类实现
首先我们需要知道线程池当中几个重要的变量
- 我们要知道线程池当中最多能放几个线程
- 当前线程池中有多少个线程正在跑任务
- 类型为任务类的一个队列,存放线程来实现任务
- 保证线程安全的互斥锁
- 保证有序的条件变量
- 清理线程时退出标志
//线程池当中 初始化的时候,线程的数量
size_t ThreadCapacity_;
//当前线程池有多少个数量
size_t ThreadCurNum_;
//线程安全的队列
std::queue<ThreadTask*> Que_;
//要实现线程安全必须要互斥
pthread_mutex_t Mutex_;
//要实现同步
pthread_cond_t Cond_;
//线程退出的标志
bool IsQuit_;
接下来就是构造函数对相应变量进行初始化
ThreadPool()
{
ThreadCapacity_ = THREADCOUNT;//THREADCOUNT 是一个宏,我们在这里就创建四个线程
//创建线程时进行统计线程数量
ThreadCurNum_ = THREADCOUNT;
pthread_mutex_init(&Mutex_,NULL);
pthread_cond_init(&Cond_,NULL);
//创建线程
bool IsCreate = ThreadCreate();
if(!IsCreate)
{
printf("ThreadPool Create thread false\n");
exit(1);
}
}
创建线程
然后我们要创建相应的线程
在此要注意,因为创建线程函数pthread_create中第三个参数线程入口函数的参数为void*arg
,所以我们的线程入口函数设置为static,屏蔽掉this指针,但入口函数中需要对队列及互斥量和条件变量进行操作,所以,pthread_create第四参数就需要将this指针传进去
//创建线程
bool ThreadCreate()
{
int ret = -1;
pthread_t tid;
for(int i = 0;i < THREADCOUNT; ++i)
{
//传this指针为了访问队列
ret = pthread_create(&tid,NULL,ThreadStart,(void*)this);
//printf("%d\n,i");
if(ret != 0)
{
printf("create thread failed!\n");
return false;
}
}
return true;
}
线程入口函数
每个线程需要干的事情就是从任务队列中Pop()取任务,拿到数据,并调用任务类中的Run()完成对数据的处理
如果线程要退出,则调用退出函数
//线程的入口函数
//为什么要定义为static,因为隐蔽掉this指针,在Linux线程库中对线程入口函数的参数定义中没有this指针
static void *ThreadStart(void* arg)
{
ThreadPool* tp = (ThreadPool*)arg;//拿到线程池当中的this指针,才能操作线程池中的成员函数和变量_
while(1)
{
pthread_mutex_lock(&tp->Mutex_);
//if(IsQuit) //线程不能再此处退出
while(tp->Que_.empty())
{
//如果线程要退出
if(tp->IsQuit_)
{
tp->ThreadQuit();
}
//如果队列为空,则等待线程
pthread_cond_wait(&tp->Cond_,&tp->Mutex_);
}
ThreadTask* tt;
tp->Pop(&tt);
pthread_mutex_unlock(&tp->Mutex_);
//为什么在解锁后run,由于已经拿到数据了,所以可以把锁释放掉了
tt->Run();
}
return NULL;
}
线程退出函数
//线程退出函数
void ThreadQuit()
{
//临界资源
ThreadCurNum_--;
//如果线程退出时候可能没解锁,又因为ThreadCurNum也是一个临界资源,所以,在--完后再进行解锁
pthread_mutex_unlock(&Mutex_);
pthread_exit(NULL);
}
往队列里放任务
入队列时,要保证线程安全,进行加锁,如果再加锁后入队列前,线程突然退出,则需要先释放锁,再退出,入完队列后要通知入完队列的线程进行任务
//放数据
//放的就是任务
bool Push(ThreadTask* Tt)
{
pthread_mutex_lock(&Mutex_);
//如果设置了线程当中的线程进行退出,则就不要再进行push数据了,将互斥锁释放,让线程池当中的线程退出
if(IsQuit_)
{
pthread_mutex_unlock(&Mutex_);
return false;
}
Que_.push(Tt);
pthread_mutex_unlock(&Mutex_);
//放完数据的时候要通知线程池当中的线程
pthread_cond_signal(&Cond_);
return true;
}
从队列中取出数据
//取数据
//要把队列中的数据返回出去,要用二级指针,因为*Tt是数据本身,返回要返回数据的地址,这样在外部才能拿到数据
bool Pop(ThreadTask** Tt)
{
*Tt = Que_.front();
Que_.pop();
return true;
}
清理线程池
线程执行完任务,清理
//清理线程池
bool PoolClear()
{
pthread_mutex_lock(&Mutex_);
IsQuit_ = true; //线程退出标志为真,则退出
pthread_mutex_unlock(&Mutex_);
//当线程池当中的队列没有数据的时候,线程都是阻塞再pthread_cond_wait当中,等待被唤醒
//通知一次即可
if(ThreadCurNum_ > 0)
{
pthread_cond_broadcast(&Cond_);
}
return true;
}
处理数据
线程池的任务就是将一个一个的任务放入队列并通知线程来获取任务,处理数据
这里就把数据打印一下就可以了
//告诉线程拿到数据如何处理
void Printf(int Data)
{
printf("i am Handler func, i print [%d]\n",Data);
}
主逻辑
线程池来存任务,并创建献线程和通知线程
线程来完成任务也就是对数据的处理
ThreadPool* tp =new ThreadPool();
for(int i = 0; i< 10; ++i)
{
ThreadTask* tt =new ThreadTask(i,Printf);
tp->Push(tt);
}
sleep(6);
tp->PoolClear();
delete tp;
return 0;