目录:
1.为什么会有线程池?
2.线程池是什么?
3.如何实现线程池?
引言:
线程池在后端服务器开发中是一个非常重要的基础组件,可以显著性地提升程序性能。在面试中,也会经常被面试官问到。线程池本身是一个生产者---消费者模型结构。实际上,针对于不同的业务场景,实现的方式会有多种,但是线程池本身的代码结构与设计思想是一样的,对于我们而言,弄懂线程池的代码结构是最重要的。本文,以C语言实现并讲解一个功能全面的是线程池的代码,以帮助大家深入理解线程池。
1 为什么会有线程池:
考虑这样的一个问题,一个线程在执行任务的时候,发现某一个步任务比较耗时,影响到了整个线程的运行时间,这个时候,我们该怎么处理这个问题呢?一个简单的回答是,我们可以将该步的任务放入一个子线程中,使得主线程与该任务解耦。比如,以一个服务器的网络收发为例:一个网络服务器的模型通常是一个while()死循环,不断地接受客户端的连接,可以分为4个步奏: accept,read,deal,send,deal的时候,可能是最耗时的操作。那么,我们可以将该步放入一个新的线程中。然而,客户端的连接是不定时的,也是不确定的;另外,一个线程的处理无法满足多个对于客户端请求的响应。如果一个客户端连接的请求分配一个线程,那么,随着客户端的响应,线程会不断地创建与销毁,带来了线程创建与销毁的资源开销。如果,我们可以创建一定数量的线程,并让其按照一定的顺序和规则,不断复用,那么,这些问题就解决了。这种方法被称为线程池技术。
对于一个线程池而言,比较重要的是,我们该分配多少个线程。线程数,处理的任务量与CPU核心数量,三者应该达到平衡。一个经验公式是:对于计算机本身而言,耗时任务分为两类,io密集型,cpu计算耗时型,对于cpu耗时型,一般线程数为cpu核心的数量,对于io密集型,为2*(n+1),n为cpu的数量。
2. 线程池是什么?
经过上面的介绍,我们可以知道,一个线程池的组成是: 分布任务的线程,处理任务的线程,以及把两者解耦的任务队列;发布任务的线程把任务放入任务队列中,处理任务的线程从任务队列取走任务;至于,发布任务的线程有多少个,取决于具体的业务场景,总的规则是与处理任务的线程平衡。具体而言,分布任务的线程就是生产者,处理任务的线程就是消费者,任务队列中的一个任务需要包含任务的上下文与处理函数;本文讲解的线程池是单生产者多消费者模型,也就是一个发布任务的线程,多个处理任务的线程,在多个处理任务的线程中,重要的是,线程调度的实现与负责均衡;本文的代码以最简单的mutex锁与condition条件变量作为线程调度的机制。
3.
如何现实线程池?
接口设计:1.创建线程池:包括消费线程的数量,以及任务队列的长度;2.销毁线程池:通知所有线程退出,停止生产者放入任务;3.任务队列的收取接口:构造任务,放入队列,通知线程池中的任务。大家可以想想为什么这些接口这样设计。
4.手撕线程池代码:
在前面做好了线程池的概念讲解以后,接下来,我们将手撕线程池的代码,实现一个单生产者多消费者的线程池模型。
结构体的定义:
#include <pthread.h>
typedef struct thread_pool_t thread_pool_t;
typedef void (*handler_pt) (void *);
typedef struct task_s{
handler-pt task_func; //队列处理函数
void* arg; //函数的参数,可以封装一个结构体,以实现多个参数传递
}task_t;
typedef struct task_queue_s{
uint32_t head; //头指针
uint32_t tail; //尾指针
uint32_t count; //当前任务的数量
task_t *queue; //储存任务的队列
}task_queue_t;
typedef struct thread_pool_s{
pthread_mutex_t mutex;
pthread_cond_t condition;
pthread_t *threads;//线程数组
task_queue_t task_queue;//任务队列
bool thread_poll_status ;//0为关闭,1为开启
int thrd_count; //线程的数量
int queue_size; //任务的大小
}thread_pool_t;
线程池的创建:
//线程池的创建
thread_pool_t* thread_pool_create(int thread_count, int task_queue_count){
if(thread_count<=0 || task_queue_task<=0){
std::cout<<"thread_pool_create fails"<<std::endl;
return nullptr;
}
thread_poll_t* pool=(thread_poll_t*)malloc(sizeof(thread_pool_t));
if(pool==nullptr){
std::cout<<"pool malloc fails"<<std::endl;
return nullptr;
}
memset(pool,0,sizeof(thread_pool_t));
pool->task_queue.head=pool->task_queue.tail==pool->task_queue.count=0;
pool->task_queue.queue=(task_t*)malloc(sizeof(task_t)*queue_size);
if (pool->task_queue.queue == nullptr) {
std::cout<<"pool tasks malloc fails"<<std::endl;
free(poll);
return nullptr;
}
memset(pool->task_queue.queue,0,sizeof(task_t)*queue_size);
pool->threads=(pthread_t*)malloc(sizeof(pthread_t)*thread_count);
if(pool->threads==nullptr){
std::cout<<"pool threads malloc fails"<<std::endl;
free(poll);
free(pool->task_queue.queue);
return nullptr;
}
for (int i=0; i < thrd_count; i++) {
if (pthread_create(&(pool->threads[i]), NULL, thread_worker, void* p) != 0) {
std::cout<<"pool threads create fails"<<std::endl;
free(poll);
free(pool->task_queue.queue);
return nullptr;
}
pool->thread_count++;
}
pool->thread_poll_status=true;
pool->task_queue_size=queue_size;
return pool;
}
给线程池中的任务队列增加一个任务,需要加锁,同时加入条件变量唤醒线程。
int thread_pool_post(thread_pool_t *pool, handler_pt func, void *arg) {
if(pool==nullptr || func==nullptr || arg==nullptr){
std::cout<<" input arguments wrong"<<std::endl;
return -1; //这里可以加错误处理函数
}
task_queue_t *task_queue = &(pool->task_queue);
if(task_queue==nullptr){
std::cout<<" pool task_queue wrong"<<std::endl;
return -2; //这里可以加错误处理函数
}
int ret=0;
//给队列加锁,使用自旋锁会更优
ret=pthread_mutex_lock(&(pool->mutex));
if(ret!=0){
std::cout<<" mutex lock wrong"<<std::endl;
return -3; //加锁失败
}
if (pool->thread_poll_status ==false) {
pthread_mutex_unlock(&(pool->mutex));
std::cout<<" thread_poll status wrong"<<std::endl;
return -3;//线程池未开启
}
if (task_queue->count == pool->queue_size) {
pthread_mutex_unlock(&(pool->mutex));
std::cout<<" pool queue is full"<<std::endl;
return -4; //任务队列已满
}
task_queue->queue[task_queue->tail].func = func;
task_queue->queue[task_queue->tail].arg = arg;
task_queue->tail =( task_queue->tail+1) % pool->queue_size;//这里是取余操作
task_queue->count++;
ret=pthread_cond_signal(&(pool->condition));
if(ret!=0){
pthread_mutex_unlock(&(pool->mutex));
std::cout<<" cond_signal wrong"<<std::endl;
return -5;
}
pthread_mutex_unlock(&(pool->mutex));
return 0;
}
线程池中线程的职责:1.取出任务 2.执行任务 3. 线程调度
void * thread_worker(void *thrd_pool) {
thread_pool_t *pool = (thread_pool_t*)thrd_pool;//线程池入口函数
task_queue_t *que;
task_t task;
while(1) {
pthread_mutex_lock(&(pool->mutex)); //加锁
que = &pool->task_queue; //取出任务
while (que->count == 0 && pool->thread_pool_status ==false) {
pthread_cond_wait(&(pool->condition), &(pool->mutex));//阻塞线程
//加while()循环是防止系统本身虚假唤醒线程,或者业务逻辑不严谨
}
//使用条件变量进行线程调度
if (pool->thread_poll_status==false) break;
task = que->queue[que->head];
que->head = (que->head + 1) % pool->queue_size; //循环队列
que->count--;
pthread_mutex_unlock(&(pool->mutex));//解锁
(*(task.func))(task.arg); //执行任务
}
pthread_mutex_unlock(&(pool->mutex));//解锁
pthread_exit(NULL); //退出线程
线程池的销毁: 1.等待所有的线程安全退出 2.释放已经申请的资源,包括锁,条件变量,内存资源。
1.等待所有线程安全退出:
int wait_all_done(thread_pool_t *pool) {
int i, ret=0;
for (i=0; i < pool->thrd_count; i++) {
if (pthread_join(pool->threads[i], NULL) != 0) {
ret=1;
}
}
return ret; //成功返回1,失败返回0
}
2. 释放资源:
void thread_pool_free(thread_pool_t *pool) {
if (pool == NULL) {
return;
}
if (pool->threads) {
free(pool->threads);
pool->threads = NULL;
pthread_mutex_lock(&(pool->mutex));
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->condition);
}
if (pool->task_queue.queue) {
free(pool->task_queue.queue);
pool->task_queue.queue = NULL;
}
free(pool);
}
3.销毁线程池的函数
int thread_pool_destroy(thread_pool_t *pool) {
if (pool == NULL) {
return -1;
}
if (pthread_mutex_lock(&(pool->mutex)) != 0) {
return -2;
}
if (pool->thread_pool_status==false) {
thread_pool_free(pool);
return -3;
}
//通过广播信号唤醒所有等待在条件变量pool->condition上的线程,并让它们重新竞争
if (pthread_cond_broadcast(&(pool->condition)) != 0 || pthread_mutex_unlock(&(pool->mutex)) != 0) {
thread_pool_free(pool);
return -4;
}
wait_all_done(pool);
thread_pool_free(pool);
pool->thread_pool_status=false;
return 0;
}
总结:
总的来说,线程池包括一个任务队列(一般是循环队列),放入任务的线程(生产者),处理任务的线程(消费者);主要的技术是通过mutex 和condition实现临界资源的互斥访问与线程的等待与通知。具体使用要结合业务场景进行变化,变生产者,任务队列,消费者中的其一或者其二,以实现性能的最好。
-----------------------------------------------------------------------------------------------------------------------end