线程池在实际的服务器开发是非常重要的一环,他涉及的概念也比较多,例如线程的使用,互斥锁,条件变量,信号量的创建使用时机等等。同时你还要知道它如何自动销毁和创建,实现一个较为智能的模式。
本文对线程池的一种构建方式进行详细分解解读注释,但也有许多需要改进的地方。
完整的代码文中已经给出,如需整个测试项目,私信发。
目录链接
2021-09-08
:
- 可以思考几个问题,为什么创建线程池,内部设计。
- 可以想象成一个链表放包装好的函数叫做任务,许多独立的线程在等待(有时间限制,超过时间就退出线程了),不断判断有没有任务,如果有了就去取。有一个添加的函数,专门把任务挂起来,然后看看有没有空闲的线程,如果有啥事不用做,没有就创建线程,执行
thread_routine
这个不断取任务函数执行。当然也有第二种策略
,线程不要超时等待退出和循环判断有没有任务,就一直wait阻塞等待,如果执行添加的函数,就去看看有没有空闲的线程,如果有就去唤醒一个线程执行任务。
1 为什么要epoll创建一个线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程的创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程为稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
2 线程池的实现流程
- 创建一个线程池,初始化其中属性,可创建一定数量线程,放入队列,或者不初始化
- 线程都处于阻塞等待状态,不占用cpu,当超时等待,可以自己结束线程
- 当需要执行函数,则把函数包装成任务,并把任务传入空闲线程进行运行
- 当没有空闲进程且小于最大线程数要求,则创建线程执行任务
- 执行完任务的线程不需要退出,阻塞等待即可(或者设置等待时间)
- 若有线程池销毁通知,确保任务执行完退出销毁
3 准备工作,封装互斥锁和条件变量
其实这边的封装就是为了方便使用,我们把信号量和互斥锁封装成一个结构体是有很大帮助的。
在实际使用中我们都知道实现互斥访问(不懂可以参考生产者和消费者模型
)其中一种方式就是条件变量和互斥锁的组合使用。
多个线程可以理解成多个消费者
。所以设计到很多共享资源,需要互斥锁。而条件变量是为了方便通知线程有可以执行的任务了。
condition.h
#ifndef _CONDITION_H_
#define _CONDITION_H_
#include <pthread.h>
//结构体内放了一个锁和条件变量
typedef struct condition
{
pthread_mutex_t pmutex;
pthread_cond_t pcond;
} condition_t;
//初始化锁和条件变量
int condition_init(condition_t *cond);
//上锁
int condition_lock(condition_t *cond);
//解锁
int condition_unlock(condition_t *cond);
//阻塞等待唤醒
int condition_wait(condition_t *cond);
//设置时间等待
int condition_timedwait(condition_t *cond, const struct timespec *abstime);
//随机唤醒一个阻塞的
int condition_signal(condition_t *cond);
//广播
int condition_broadcast(condition_t *cond);
//销毁条件变量和互斥锁
int condition_destroy(condition_t *cond);
#endif /* _CONDITION_H_ */
# condition.c
#include "condition.h"
int condition_init(condition_t *cond)
{
int status;
if ((status = pthread_mutex_init(&cond->pmutex, NULL)))
return status;
if ((status = pthread_cond_init(&cond->pcond, NULL)))
return status;
return 0;
}
int condition_lock(condition_t *cond)
{
return pthread_mutex_lock(&cond->pmutex);
}
int condition_unlock(condition_t *cond)
{
return pthread_mutex_unlock(&cond->pmutex);
}
int condition_wait(condition_t *cond)
{
return pthread_cond_wait(&cond->pcond, &cond->pmutex);
}
int condition_timedwait(condition_t *cond, const struct timespec *abstime)
{
return pthread_cond_timedwait(&cond->pcond, &cond->pmutex, abstime);
}
int condition_signal(condition_t *cond)
{
return pthread_cond_signal(&cond->pcond);
}
int condition_broadcast(condition_t* cond)
{
return pthread_cond_broadcast(&cond->pcond);
}
int condition_destroy(condition_t* cond)
{
int status;
if ((status = pthread_mutex_destroy(&cond->pmutex)))
return status;
if ((status = pthread_cond_destroy(&cond->pcond)))
return status;
return 0;
}
4 线程池的实现
4.1 结构说明(重)
线程池主要组成就三个部分
- task_t类型的结构体
- threadpool_t类型结构体
- 四个实现函数
4.1.1 任务结构体
任务结构体简单的说就是对实际执行函数的封装
特别注意一下第一个成员变量,其实就是
函数指针
,函数指针 的本质是一个指针,该指针的地址指向了一个函数,所以它是指向函数的指针。简单的说:理解成一个未初始化的函数变量
把函数指针和参数分开作为参数其实就是为了在调用pthread_create方便传入参数。第三个成员和链表的下一个指针域一个意思。我们这边吧任务串成链表。
typedef struct task
{
// 任务回调函数
// 简单的说就是返回值为空指针的函数指针
void *(*run)(void *arg);
// 回调函数参数
void *arg;
struct task *next;//指向的下一个结构体指针
} task_t;
4.1.2 线程池结构体
定义那么多关于线程的变量其实都是为了方便我们去管理线程。
唯一需要注意的是任务队列的头指针和尾指针
。因为定义了这个,我们可以通过线程池对象成员很方便的放入或者取出任务。重要
typedef struct threadpool
{
condition_t ready; //初始化了一个条件变量和互斥锁
task_t *first; //任务队列头指针
task_t *last; //任务队列尾指针
int counter; //线程池中当前线程数
int idle; //线程池中当前正在