目录
本文仅代表个人观点,如有疏漏之处望评论区回复指正,感谢!
线程池
what
一种多线程的使用和管理模式,大多用于高并发服务器上,能够合理有效地利用多线程模型高并发服务器上的资源,多用于linux高并发服务器的场景下,与epoll模型结合使用。
why
- 服务器角度:解决多线程维护的问题。分为两方面,线程创建数量控制与线程资源的复用。在数量控制方面,线程过多会带来调度的额外开销,在线程池中通过主控线程可以合理维护线程数量;在线程资源的复用方面,避免了处理短时间任务时频繁创建销毁线程的消耗,处理完成任务后回归线程池,通过主控线程管理,充分利用内核资源。
- 客户端角度:解决客户端连接服务器的延时问题。每一个客户端连接服务器都会分配一个线程去处理IO任务,如果没有预先创建好的线程和主控线程合理维护,客户端连接时需要等待服务器创建线程产生延时,影响用户体验。采用线程池,服务器可以迅速响应客户端的连接,分配线程处理任务。
how
解读线程池的设计思路以及设计过程中遇到的问题。
整体设计
分为三部分:管理线程(负责线程数的扩容与缩减)、任务队列(负责向任务队列填装任务)、工作线程(负责从任务队列取任务并处理)。
其中任务队列、工作线程采用生产者-消费者模型,用互斥量+条件变量实现线程同步。
工作情况分析
1.线程池刚刚初始化,任务队列中没有任务,线程池中只有默认线程数(假设为3),线程处于空闲等待状态。
2.添加小于默认线程数量的任务,线程池开始工作,此时线程池忙线程数 < 总存活线程数的70%
主线程添加两个任务,任务队列通过pthread_cond_signal通知线程池,唤醒线程池从任务队列取任务并处理。
3.再添加一个任务到任务队列中,通知线程取任务工作,此时线程池忙线程数 > 总存活线程数的70%
管理线程为线程池扩容,单次扩容数为一个固定线程数目(假设为3)
4.某个线程执行任务完成恢复空闲状态,此时线程池中忙线程个数*2 < 总存活线程数,且总存活线程数 > 线程池最小线程数
管理线程通知线程自动退出,单次缩减数为一个固定线程数目(假设为3)
线程池的三种socket模型:
- 每个线程都阻塞于accept,当有客户端连接时,所有线程都被唤醒竞争,谁先抢到谁处理连接,其它线程继续阻塞。
缺点:产生惊群效应,见:https://blog.csdn.net/lyztyycode/article/details/78648798
- 一个主线程负责阻塞accept,其它线程等待主线程分配accept连接处理。
缺点:会导致主线程的灵活性较差,当大量客户端连接时,主线程可能由于处理线程太多而卡死,导致整个进程崩溃。
- 引入互斥锁,每个线程先争一把锁,谁抢到锁,谁去accept。
性能较好,实现简单。本文描述的线程池采用这种方式实现。
实现细节分析
参数设计:
线程池状态参数
int thread_min; //最小线程数(初始值)
int thread_max; //最大线程数
int thread_alive; //存活线程数
int thread_busy; //忙线程数
int thread_kill; //待退出线程数
bool shutdown; //线程池开关
任务队列参数
struct task_t{
void* (*fun)(void*); //对任务进行封装的函数指针
void* arg; //函数指针参数
};
task_t* task_queue; //任务队列
int queue_head; //任务队列头索引
int queue_rear; //任务队列尾索引
int queue_cur; //任务队列当前索引
int queue_max; //任务队列最大值
线程同步控制
pthread_mutex_t lock;
pthread_cond_t queue_not_empty;
pthread_cond_t queue_not_full;
其它
pthread_t* works_tid; //保存工作线程的数组
pthread_t manager_tid; //管理线程
函数接口设计:
bool InitPool(int thread_min,int thread_max,int queue_max); //初始化线程池参数
void FreePool(); //释放线程池资源
void DestroyPool(); //销毁线程池
static void* Worker(void* arg); //工作线程(消费者)
static void* Manager(void* arg); //管理线程
bool AddTask(void* (*fun)(void* arg),void* arg); //添加任务(生产者)
bool is_thread_alive(pthread_t tid); //检测线程是否存活
具体实现代码(附详细注释+测试用例)请移步我的github:https://github.com/boomshakalakaaa/ThreadPool
一些常见问题
1.如何正确设置线程池的线程数?
2.服务器压力增大或空闲时,怎么处理线程池中线程数,什么时候增加或减少线程数,一次增加或减少多少?
答:设置一个人为规定的经验值,比如忙线程数占存活线程数的70%或80%。即线程池当前处于一个所有线程都即将得到任务处理的状态,假设线程处理的任务短期内不能完成,线程池中的线程不能得到复用,为了避免客户端连接服务器时需要等待线程创建和资源分配从而产生高延时,在忙线程数占存活线程数的70%或80%时就开始给线程池扩容,每一次扩容的值可以是线程池的初始线程数。如果扩容值是固定值,那么要注意,设置其值应该满足:线程池初始线程数 + 单次扩容数*k = 线程池最大线程数,其中k为最大扩容次数。否则线程池在最后一次扩容时将无法扩容,达不到线程池设置的最大线程数。
3.任务队列的最大数量该如何设置,超出最大任务数怎么处理?
答:任务队列的最大数量相当于一个线程池的最大任务并发数,也就是服务器能支持的最大并发数。这个要根据服务器系统资源和并发要求来配置,假如服务器最大的并发量在50000,超过50000服务器系统资源不足会卡死,则尽量设定一个比50000稍小的数值,避免服务器宕机。如果有需要控制服务器并发量在某个最大值,则可以根据需要按需设置这个任务队列的最大数量,总之它是一个按需根据实际场景设置的值。如果超出最大任务数,即代表当前已经超出了服务器可承受的最大并发量,这就是需要采用分布式负载均衡解决的问题了。如果并发量实在太大,可以采用双缓冲队列,将任务投递到另一个阻塞队列中等待处理。或者可以直接拒绝任务,等待服务器并发量减小后,客户端再次发起连接对其进行处理。
4.为什么采用互斥量+条件变量,而不是采用信号量实现线程同步?
见之前的文章:操作系统总结系列之【线程同步】https://blog.csdn.net/qq_37348221/article/details/113102629