线程池(thread pool)
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
应用范围
1、需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2、对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并现"OutOfMemory"的错误。
线程池组成部分
1、线程池管理器(ThreadPoolManager):用于创建并管理线程池
2、工作线程(WorkThread): 线程池中的线程
3、任务接口(Task): 每个任务必须实现的接口(回调函数),以供工作线程调度任务的执行。
4、任务队列 : 用于存放没有处理的任务。提供一种缓冲机制。
如何让相同入口函数的线程,处理不同的请求
1.switch case语句 : 但是处理大量不同需求的时候,case会太多造成麻烦。
2.向线程池抛入数据的时候,将处理该数据的函数一起抛入(函数地址) , 线程池当
中的线程只需要调用传入的函数处理传入的数据即可;
线程池生产者消费者模式示例
线程池生产者消费者模式,是比较常见的实现方式,比较简单。分为同步层、队列层、异步层三层。同步层的主线程处理工作任务并存入工作队列,工作线程从工作队列取出任务进行处理,如果工作队列为空,则取不到任务的工作线程进入挂起状态。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
#include <queue>
//线程池 = 线程安全队列 + 一大堆的线程
//线程安全队列
// 元素 = 数据 + 处理数据的函数地址
//- 创建固定数量的线程池,然后循环从任务队列中获取任务对象
//- 获取任务对象后,然后执行任务对象中的任务接口
#define THREADCOUNT 4
typedef void* (*Handler_t)(int);//回调函数类型,线程任务接口
class ThreadTask //任务队列存储自定义元素类型
{
public:
ThreadTask(int data, Handler_t handler)
{
data_ = data;//传入数据
handler_ = handler;//传入处理数据的函数指针
}
~ThreadTask()
{
//
}
//使用函数,处理数据
void Run()
{
handler_(data_);
}
private:
int data_;
Handler_t handler_; //返回值为void* 参数为int
};
//线程池结构
class ThreadPool
{
public:
ThreadPool()
{
capacity_ = 10;//任务队列容量
thread_capacity_ = THREADCOUNT;//线程数
pthread_mutex_init(&lock_, NULL);//初始化用于线程安全的互斥量
pthread_cond_init(&cond_, NULL);//初始化同步的条件变量
//创建线程
bool is_create = ThreadCreate();
if(!is_create)
{
printf("Threadpool Create thread failed\n");
exit(1);
}
IsExit = false;//定义一个bool标志位来判断线程是否退出,注意也是一个临界资源,操作时要保证互斥
cur_thread_count_ = THREADCOUNT;//闲置的线程数
}
~ThreadPool()
{
}
bool Push(ThreadTask* tt)//往线程池里的任务队列里 放数据和任务接口
{
pthread_mutex_lock(&lock_);
if(IsExit)
{//线程已推出 ,无法再放任务
pthread_mutex_unlock(&lock_);//注意返回前要解锁资源
return false;
}
que_.push(tt);//数据元素入队
pthread_mutex_unlock(&lock_);
//当插入数据之后 通知线程池当中的工作线程(消费者),完成同步
pthread_cond_signal(&cond_);
return true;
}
bool Pop(ThreadTask** tt)//取队列里的元素
{
*tt = que_.front();
que_.pop();
return true;
}
/*
问题:从队列当中拿数据和处理业务数据的逻辑要是混合在一起了
假设如果处理数据特别漫长,而只有一个线程在处理数据,而其他线程在等待获取队列当中元素,显然不是使用线程池想要的结果,所以采用的方式:将从队列当中拿数据和处理业务数据解耦开来
加锁的时候:只需要保证拿数据的时候是互斥的
处理业务数据的时候,多个线程可以并行的去运行
*/
void ThreadJoin()//等待线程退出,回收资源
{
for(int i = 0; i < THREADCOUNT; i++)
{
pthread_join(tid_[i], NULL);
}
}
// 如果直接退出线程,则有可能队列当中当中还有数据没有处理完毕;
// 我们不应该去调用这样的接口来结束我们的线程
void ThreadExit()
{
for(int i = 0; i < THREADCOUNT; i++)
{
pthread_cancel(tid_[i]);//线程终止
}
}
void ThreadPoolClear()
{
//标志位
pthread_mutex_lock(&lock_);
IsExit = true;//线程池里的线程可以退出了
pthread_mutex_unlock(&lock_);
if(cur_thread_count_ > 0) //如果还有闲置的线程
{
pthread_cond_broadcast(&cond_);//唤醒等待,去判断队列是否为空,进而,会走到判断条件,判断是否可以退出
}
}
private:
static void* ThreadStart(void* arg)//线程函数只有一个参数,所以使用static声明
{
ThreadPool* tp = (ThreadPool*)arg;
while(1)
{
//从队列当中获取数据,进行消费 对于不同的线程而言,在获取数据的时候,是互斥的
pthread_mutex_lock(&tp->lock_);
ThreadTask* tt;
while(tp->que_.empty())//队列里没有资源,判断是否可以安全退出
{
//判断是否可以进行结束
if(tp->IsExit)//判断退出条件
{
//退出
tp->cur_thread_count_--;
pthread_mutex_unlock(&tp->lock_);
pthread_exit(NULL);
}
//不退出
//调用条件变量 等待接口
pthread_cond_wait(&tp->cond_, &tp->lock_);
//进入条件变量的pcb等待队列,等待被唤醒
}
//有资源,则先取队列里的数据元素
tp->Pop(&tt); //传入指针的地址,出参
pthread_mutex_unlock(&tp->lock_);
///保证取数据时是互斥的,而线程处理可以多个线程并发
//调用队列当中元素提供的函数,去处理数据 对于线程走到该位置的时候,就可以并行的处理了
tt->Run();//拿到队列元素后,调用任务接口
//防止内存泄漏
delete tt;
}
}
bool ThreadCreate()
{
for(int i = 0; i < THREADCOUNT; i++)
{
int ret = pthread_create(&tid_[i], NULL, ThreadStart, (void*)this);
if(ret != 0)
{
perror("pthread_create");
return false;
}
}
return true;
}
private:
std::queue<ThreadTask*> que_;/线程池的存放资源和任务接口的队列
size_t capacity_;//资源缓冲容量
//互斥
pthread_mutex_t lock_;
//同步 消费线程的条件变量,但是并没有生产线程的条件变量
//由于我们所说的,客户端的请求行为我们是无法控制的。所以就不需要通知生产者来进行生产,当生产线程有了数据,就直接往线程池当中抛入就可以了,在通知消费线程来进行消费
pthread_cond_t cond_;//只需要一个条件变量即可
//线程池当中的初始化的时候线程数量
size_t thread_capacity_;
//标识具体还有多少线程数量
size_t cur_thread_count_;
//保存线程池当中的线程的线程标识符
pthread_t tid_[THREADCOUNT];
//标志是否可以退出
bool IsExit;
};
void* DealData(int data)//线程池里的里任务队列里数据元素里的任务接口
{
printf("consume data is %d\n", data);
return NULL;
}
int main()
{
ThreadPool* tp = new ThreadPool();
//在这个代码当中main函数的线程,就充当生产线程,往线程池的线程安全队列当中push数据
for(int i = 1; i <= 50; i++)
{
ThreadTask* tt = new ThreadTask(i, DealData);//理解为封装数据和任务接口
tp->Push(tt);//再传入任务队列,消费线程拿到后实现任务逻辑
}
//等待线程池当中线程退出
sleep(15);
tp->ThreadPoolClear();//线程池里的线程退出
tp->ThreadJoin();//回收线程池里的线程退出后的资源
delete tp;
return 0;
}
线程池里的线程何时退出
退出问题:
要是线程直接退出,但是线程池当中线程安全队列里面可能还有数据没有处理,即对于客户端的请求没有应答问题。
所以要仔细判断线程池里的线程的几种状态:
线程池当中线程可能存在的几种情况
1.加互斥锁
–》加锁–》判断队列是否为空 --》IsExit 为true–》pthread_ exit
2.调用pthread_ cond. wait当中阻塞在pthread_ cond. wait接口当中
由于阻塞在等待接口当中,要退出时,将其唤醒,也还是要判断队列里是否有资源可以访问,即while–》加锁–》判断队列是否为空–》IsExit–》pthread_exit
3.在队列当中获取数据获取成功–》处理数据–》- while --》加锁–》判断队列是否为空–》IsExit --》 pthread_ exit
4.正在处理队列里面的数据》处理数据— while --》加锁–》判断队列是否为空–》IsExit --》 pthread. exit
结论:只有当线程判断了队列当中没有数据的情况下才可以退出。
//由于本代码里,我们知道会有多少资源进队列,但是事实上客户端的请求是不确定的,所以要加标志位判断来使线程退出
//线程处理函数里的判断条件
if(tp->IsExit)//判断退出条件
{
//退出
tp->cur_thread_count_--;
pthread_mutex_unlock(&tp->lock_);
pthread_exit(NULL);
}
//控制线程推出
void ThreadPoolClear()
{
//标志位
pthread_mutex_lock(&lock_);
IsExit = true;//线程池里的线程可以退出了
pthread_mutex_unlock(&lock_);
if(cur_thread_count_ > 0) //如果还有可能等待的线程
{
pthread_cond_broadcast(&cond_);//唤醒等待,去判断队列是否为空,进而,会走到判断条件,判断是否可以退出,进而退出
}
}