文章目录
线程池基础
认识线程池
首先,线程池是一个什么东西,简单从低层次来讲,线程本身是实现并发的一种机制,可以做到多任务同时进行或者说多线程去完成一个任务。但是线程去实现高并发的操作过程过于复杂,那么线程池的出现 ,就是为了用简单,快捷,高效率的去实现高并发。那原理很简单,顾名思义,线程池可以理解为多个线程的集合,就是将多个线程伴随这服务一起启动,然后待命。当有任务到来时,他会自动分配去执行,当线程池线程不够用时,也会自动添加线程,并且当任务完成或者说并发量低时,线程池也会自动去删除一些不需要或者说是空闲的线程来减少内存的占用,来增加工作效率。简单理解一下线程池的定以后,我们一起来往下看,来研究一下线程池是怎么做的吧!
线程池的构造
线程池概念
线程池并不是一个操作系统或者说哪个协会提出的一些标准的机制,线程池完全脱离了这两个理念,是一个完全由程序员写出来的一个小功能,也就是说代码并不唯一而且并没有现成的供你调用的借口,所以说我们需要一步一步的去将它实现。
首先,我们来看一下他的工作原理和内部结构,来研究一下到底是怎么个线程池!
线程池的内部构造
线程池的内部有三个部分构成,每个部分都有自己不可欠缺的一个角色:
第一部分:任务队列
任务队列本身是一个队列,具备先进先出的原则,需要自己去实现。那任务队列的作用就是去对外接受任务,对内提供任务。
第二部分:管理者线程
管理者线程本身也是一个线程,但是与线程池其他线程干的活是不一样的。管理者线程的作用 就是自动去控制去分配去安排线程去干哪些活,然后适当去管理线程的创建和销毁。比较智能化。
第三部分:一堆线程
一堆线程就可以理解为小兵,全权听从管理者的安排,干活就干活,出生就出生,销毁就销毁!
三者概念清楚了以后咱们来看一下线程池的工作框图
通过上图,可以看出一个问题,线程是如何知道任务到来的:当线程池运行起来任务还未到来时,全体线程除了管理者线程之外都会调用条件变量进行挂起等待,当任务队列中有任务到来时,任务队列会发送一个任务到来的signal信号,也就是生产者和消费者的关系,然后唤醒其中一个线程去处理这个任务。
以上为线程池的所有概念性知识点,现在应该滤清了线程池的工作原理,只是不知道他是怎么做到的
所以接下来我们将开始一步步的实现并且配合这最详细的讲解
线程池实现
构建一个线程池
首先我们要构建一个线程池,听着是不是有点向面向对象的感觉,没错,我们需要用面向对象的思想,但是我们需要用C语言实现,所以一般我们选用一个结构体来描述一个线程池。
首先要思考一下线程池中都应该有什么样的东西。
任务队列:
1.其实任务对于我们而言就是一个执行函数
2.然后执行函数需要传递参数
3.任务队列中最多可以存储多少个任务,最大限制
4.任务队列中的当前任务数量。
5.任务队列中的头部
6.任务队列中的尾部
管理者线程:
1.因为我们需要创建一个管理者线程,那么这个线程
在线程池中应该有一个管理者线程ID号作为他的体现。
一堆线程:
1.同样,存放线程池中干活的线程我也需要一个存放线程号的数组
2.线程池启动时最少一起起来的线程数–最小的线程个数
3.线程池最大能支持多少个线程一起去工作–最大线程的个数
4.线程池中正在工作的线程个数—忙碌的线程个数
5.线程池中目前可以正常工作的线程个数—存活的线程个数
6.当线程池发现任务很少时需要自动销毁线程到最小的线程个数
所以还需要一个销毁线程的个数(后面一看代码就明白了)
其他辅助部分
1.三个条件变量:
1.用于任务队列满时使用
2.用于任务队列空时使用
3.用于长时间未使用消息队列是使用(根据个人需求)
2.二个互斥锁:
1.需要一个互斥锁锁住整个线程池的结构体,因为他属于临界资源
2.需要一个互斥锁锁住忙碌线程个数的结构体,因为这是经常被调用的临界资源
3.线程池状态
给线程池设定一个标注位,来标识线程池的运行还是销毁
注:有些东西可能不是很理解为什么有他的存在,只因为线程池本身就是程序员完全手写出来的东西,不会那么固定,因为本人已经看透线程池原理,所以说可以如此去制定,放心后面会将你的所有疑惑全部解开。
让我们一起来看一下线程池结构体 pthreadpool 的详细信息。
//任务结构体
typedef struct task {
void (* fun)(void * arg);//指向要执行任务的一个函数指针
void * arg; //传送给该函数的一个参数
}task; //定义一个新的类型 task
//---------------------主体----**线程池结构体**--------------------------
typedef struct pthreadpool{
//----------------------任务队列部分-------------------
task * taskQ; //任务队列--数组 --后续会为他开空间,方便控制大小
int queueMaxSize; //任务队列的容量
int queueSize; //当前任务的个数
int quueFront; //队头
int queueRear; //队 尾
//----------------------管理者ID
pthread_t managerID;
//-----------------------工作线程ID
pthread_t * thread_tID; //因为多个所以说直接用一片地址
//-----------------------线程池相关信息
int minnum;//最小的线程个数
int maxnum;//最大的线程个数
int busynum;//忙碌的线程个数
int livenum;//现在线程池可用的线程--存活的线程个数
int exitnum;//销毁的线程个数
//----------------保护临界资源的机制--互斥锁
pthread_mutex_t poolmutex; //锁住整个线程池
pthread_mutex_t busymutex; //锁住busynum 变量
//-----------------线程同步唤醒机制--条件变量 来监控任务队列状态
pthread_cond_t notfull; //判断任务队列是不是满了
pthread_cond_t notempty; //判断任务队列是不是空了
pthread_cond_t pooldestory; //线程池工作空闲关闭判断条件变量
//线程池的状态1.运行 2.销毁
int shutdown;//1代表被销毁,0代表正在运行
} pthreadpool;
线程池所需函数
线程池的行态已经做出,然后我们需要构想一下线程池都需要什么函数,也就是头文件 pthreadpool.h 文件
/*函数介绍---------------- init_pthreadPoolCreat---------------
作者:郑文杰
功能:创建线程池并且初始化
参数:
minnum :线程池最小个数
maxnum :线程池最大个数
queueMaxSize :任务队列中最大的任务个数
返回值:会将创建好的线程池的地址返回 失败返回NULL 并打印错误信息
*/
pthreadpool * init_pthreadPoolCreat(int minnum,int maxnum,int queueMaxSize);
/*函数介绍---------------- pthreadPoolDestory ---------------
作者:郑文杰
功能:销毁线程池
参数:
pool:线程池的首地址
返回值:销毁成功返回0 失败返回-1 并打印错误信息
*/
int pthreadPoolDestory(pthreadpool * pool);
/*函数介绍---------------- ThreadPoolAdd ---------------
作者:郑文杰
功能:向线程池中添加任务
参数:
pool:线程池首地址
fun :任务函数指针
arg :任务函数参数
返回值:无
*/
void ThreadPoolAdd(pthreadpool * pool,void(*fun)(void *),void * arg);
/*函数介绍---------------- GetBusyNum ---------------
作者:郑文杰
功能:获取线程池正在工作的线程个数
参数:
pool:线程池首地址
返回值:这个函数永远时成功的 成功返回获取到的正在工作的线程个数
*/
int GetBusyNum(pthreadpool * pool);
/*函数介绍---------------- GetBusyNum ---------------
作者:郑文杰
功能:获取线程池中活着的线程个数
参数:
pool:线程池首地址
返回值:这个函数永远时成功的 成功返回获取到的存活的线程个数
*/
int GetLiveNum(pthreadpool * pool);
/*函数介绍---------------- myfunc ---------------
作者:郑文杰
功能:正常工作线程函数--不会被用户主动调用
参数:
arg :工作函数的参数
返回值:函数执行失败线程直接结束,如果执行成功会返回 "线程执行完毕" 字符串
*/
void * myfunc (void * arg);
/*函数介绍---------------- manager ---------------
作者:郑文杰
功能:管理者线程函数--不会被用户主动调用 --主要负责监控线程池自动调节
参数:
arg :工作函数的参数
返回值:该函数不会失败,退出时返回NULL
*/
void * manager(void * arg);
/*函数介绍---------------- manager ---------------
作者:郑文杰
功能:线程结束释放资源
参数:
pool :工作函数的参数
返回值:无
*/
void pthreadExit(pthreadpool * pool);
以下就是函数的实现
创建并且初始化线程池
pthreadpool * init_pthreadPoolCreat(int minnum,int maxnum,int queueMaxSize)
{
//创建一个指向线程池的指针,并且开辟空间
pthreadpool * pool = (pthreadpool * )malloc(sizeof(pthreadpool));
do //使用do-while方便管理return 函数最后有体现
{
if (pool == NULL)
{
printf("创建线程池失败,第1次\n");
break;
}
//给线程池存放线程号的头指针开辟空间
pool->thread_tID = (pthread_t *)malloc(sizeof(pthread_t) * maxnum);
if (pool->thread_tID == NULL)
{
printf("创建任务队列失败,第2次\n");
break;
}
//初始化申请到的内存
memset(pool->thread_tID,0,sizeof(pthread_t) * maxnum);//初始化为0 用于后续线程号存放判断使用
//初始化线程池信息
pool->minnum = minnum; //线程池最小个数
pool->maxnum= maxnum; //最大个数
pool->busynum = 0; //忙碌线程
pool->livenum = minnum;//存活的线程数,刚被创建时线程数最小
pool->exitnum = 0; //销毁线程数
//初始化锁
if (pthread_mutex_init(&pool->poolmutex,NULL) != 0 || pthread_mutex_init(&pool->busymutex,NULL) != 0
|| pthread_cond_init(&pool->notfull,NULL) != 0 || pthread_cond_init(&pool->notempty,NULL) != 0)
{
printf("初始化锁或者初始化条件变量失败\n");
break;
}
//任务队列初始化
pool->taskQ = (task *)malloc(sizeof(task) * queueMaxSize); //为队列开辟内存
pool->queueMaxSize = queueMaxSize; //容量固定
pool->queueSize = 0; //任务个数
pool->quueFront = 0; //头
pool->queueRear = 0; //尾
pool->shutdown = 0;//标识线程时被创建
//创建管理者线程,管理者要去运行manager函数,直接把整个线程池传过去
if(pthread_create(&pool->managerID,NULL,manager,pool)!= 0 )
{
printf("创建管理者线程失败\n");
break;
}
//创建线程池工作线程
int i = 0;
for (i = 0; i < minnum ; i++) //用最小的个数去创建线程池
{
//工作线程去执行myfunc函数,传参也直接把pool线程池信息传过去
if (pthread_create(&pool->thread_tID[i],NULL,myfunc,pool) != 0)
{
printf("创建线程 %d 发生错误\n",i);
break;
}
}
return pool;
} while (0);
//如果发生错误会跳出while循环 开始释放资源
if (pool->thread_tID)
{
free(pool->thread_tID);
pool->thread_tID = NULL;
}
if (pool->taskQ)
{
free(pool->taskQ);
pool->taskQ = NULL;
}
if (pool)
{
free(pool);
pool = NULL;
}
return NULL;
}
管理者线程函数
//管理者线程 主要负责的是增加线程和销毁线程
void * manager (void * arg)
{
//先将参数接收一下
pthreadpool * pool = (pthreadpool *) arg;
int j = 0;
int count =40;//空闲时销毁线程池使用--根据个人需求
while (pool->shutdown == 0) //工作条件,当线程池没有被销毁时要工作
{
//管理者每1秒检测一次
sleep(1);
//监控线程池任务数和当前线程数量
pthread_mutex_lock(&pool->poolmutex);//因为在访问线程池临界资源所以需要加锁
int queuesize = pool->queueSize;
int livenum = pool->livenum;
printf("管理者第%d次检测,当前线程中任务队列任务的个数为%d,当前存活的线程个数为%d\n",j++,queuesize,livenum);
//检测任务队列中任务,如果没有任务则开始倒计时40秒,40秒之内未进入稳步则关闭线程池
if (pool->queueSize == 0 && pool->livenum == 5)
{
printf("倒计时%d秒关闭线程池\n",count);
count --;
if (count == 0)
{
pthread_cond_signal(&pool->pooldestory);//关闭线程池
}
}
pthread_mutex_unlock(&pool->poolmutex);
//监控忙碌线程个数
pthread_mutex_lock(&pool->busymutex);
int busynum = pool->busynum;
printf("当前忙碌的线程为%d\n",busynum);
pthread_mutex_unlock(&pool->busymutex);
//添加线程 ----指定一个规则来规定啥时候添加线程
//任务个数 > 当前的可用线程个数 && 可用的线程数 < 最大线程数
if ((queuesize > livenum) && (livenum < pool->maxnum))
{
printf("进入自动创建线程\n");
pthread_mutex_lock(&pool->poolmutex);
//添加线程 ,每次添加两个
int counter = 0; //计数
int i = 0;
//判断条件 循环i不能大于线程最大个数 并且 计数不能超过目标添加个数 并且 存活个数不能超过最大个数
//COUNNT_NUM 宏定义 2
for ( i = 0; (i < pool->maxnum) && (counter < COUNT_NUM )&& (livenum < pool->maxnum); i++)
{
//创建线程
if (pool->thread_tID[i] == 0) //判断存放线程号的数组中哪个可以存放线程号,如果为0可以存放
{
pthread_create(&pool->thread_tID[i],NULL,myfunc,pool);
counter ++;
pool->livenum++;
printf("自动创建线程成功\n");
}
}
pthread_mutex_unlock(&pool->poolmutex);
}
//销毁线程 同样需要指定规则
//忙的线程*2 《 存货的线程数 && 存活的线程数 > 最小线程数
if (busynum * 2 < livenum && livenum > pool->minnum)
{
pthread_mutex_lock(&pool->poolmutex);
pool->exitnum = COUNT_NUM;//每次销毁两个
//工作线程会有判断
pthread_mutex_unlock(&pool->poolmutex);
//让工作的线程自杀
int i = 0;
for ( i = 0; i < COUNT_NUM; i++)
{
pthread_cond_signal(&pool->notempty);
//因为eixtnum被赋值了,所以工作进程在被唤醒时进入了自动销毁模块。
}
}
}
return NULL;
}
工作线程函数
//工作线程
void * myfunc(void * arg)
{
pthreadpool * pool = (pthreadpool *) arg;
while (1)
{
pthread_mutex_lock(&pool->poolmutex);//操作线程池的临界区就必须加锁
//当任务队列中没有任务时工作线程阻塞等待任务的到来
while(pool->queueSize == 0 && !pool->shutdown) //判断线程池有没有被关闭 !pool->shutdown
{
//如果没有任务或者线程池关闭了,我需要阻塞线程
pthread_cond_wait(&pool->notempty,&pool->poolmutex);
//自动销毁功能,用户管理者线程自动销毁时,起一个配合作用,当管理者线程将exitnum赋值时,if循环就会进去去执行自动销毁
if (pool->exitnum > 0) //判断是否在执行销毁线程
{
pool->exitnum--; //目标销毁线程数-1
pool->livenum--;//存活的线程数-1
pthread_mutex_unlock(&pool->poolmutex);//解除锁
pthreadExit(pool);//函数调用,去看一下,下个函数
}
}
//唤醒后判断线程池是否被关闭
if (pool->shutdown == 1)
{
pthread_mutex_unlock(&pool->poolmutex);//解除锁
pthreadExit(pool); //关闭线程
//函数调用,去看一下,下个函数
//主要作用是关闭这个线程后,清楚他在线程号队列中占用的位置
}
//开始工作,从工作队列中取出任务
task mytask;
mytask.fun = pool->taskQ[pool->quueFront].fun;//从头部开始取任务
mytask.arg = pool->taskQ[pool->quueFront].arg;//从头部开始取参数
//将任务数组维护成环形队列-移动头节点
//将移动后对队列最大数进行取于操作
//对队列最大值进行取余是为了永远不超过队列的大小,到达末尾后又从头部开始
pool->quueFront = ( pool->quueFront + 1 ) % pool->queueMaxSize;
//取出了一个任务,需要将任务个数减一
pool->queueSize --;
//向任务队列中放任务的线程有可能因为任务满在被阻塞
//所以取出一个任务之后要告知任务生产者 任务队列中有一个位置可以存放任务
pthread_cond_signal(&pool->notfull);
pthread_mutex_unlock(&pool->poolmutex);//操作线程池的临界区就必须加锁
//开始调用函数
//开始工作时忙碌线程数+1
pthread_mutex_lock(&pool->busymutex);
pool->busynum ++; //忙碌线程数+1
pthread_mutex_unlock(&pool->busymutex);
mytask.fun(mytask.arg); //开始工作
//工作完成后释放空间
//free(mytask.arg);
//mytask.arg=NULL;
printf("工作完成%ld\n",pthread_self());
//工作完成后忙碌线程数-1;
pthread_mutex_lock(&pool->busymutex);
pool->busynum --; //忙碌线程数-1
pthread_mutex_unlock(&pool->busymutex);
}
return "线程执行完毕";
}
线程关闭函数
//退出线程函数
void pthreadExit(pthreadpool * pool){
pthread_t pid = pthread_self();//获取当前线程的线程号
int i = 0;
for ( i = 0; i < pool->maxnum; i++)
{
if (pid == pool->thread_tID[i])
{
pool->thread_tID[i] = 0;//主要是消除退出线程在线程号数组中占用的位置
printf("线程结束,该线程的线程号存放在数组中的位置已被清0\n");
break;
}
}
pthread_exit(NULL);
}
任务添加函数
/给线程池添加任务函数
void ThreadPoolAdd(pthreadpool * pool,void(*func)(void * ),void * arg){
pthread_mutex_lock(&pool->poolmutex);
//判断任务队列是否满了
while (pool->queueSize == pool->queueMaxSize && !pool->shutdown)
{
//阻塞生产者线程
pthread_cond_wait(&pool->notfull,&pool->poolmutex);
//那这个阻塞由谁来发消息呢?因该由工作线程在拿走一个任务后告知一下该线程,所以
//signal应该在工作线程拿走任务之后之后
}
if (pool->shutdown) //判断一下上个循环是否因为线程池被销毁而退出while循环
{
printf("线程池关闭,线程结束\n");
pthread_mutex_unlock(&pool->poolmutex);
return;
}
//添加任务,将任务添加的任务队列的队尾
pool->taskQ[pool->queueRear].fun = func;
pool->taskQ[pool->queueRear].arg = arg;
//添加完任务后,队列指针后移
pool->queueRear = (pool->queueRear + 1) % pool->queueMaxSize;//环形队列
pool->queueSize ++; //任务队列任务数加1
pthread_cond_signal(&pool->notempty);//唤醒消费者
pthread_mutex_unlock(&pool->poolmutex);
}
获取忙碌线程函数
int GetBusyNum(pthreadpool * pool)
{
pthread_mutex_lock(&pool->busymutex);
int busynum = pool->busynum;
pthread_mutex_unlock(&pool->busymutex);
return busynum;
}
获取存活线程函数
int GetLiveNum(pthreadpool * pool)
{
pthread_mutex_lock(&pool->poolmutex);
int livenum = pool->livenum;
pthread_mutex_unlock(&pool->poolmutex);
return livenum;
}
销毁线程池
int pthreadPoolDestory(pthreadpool * pool) //销毁线程池
{
pthread_mutex_lock(&pool->poolmutex);
//等待管理者线程在检测到长时间未用到线程池时会发给销毁线程池一个信号
//接到信号后向下执行
pthread_cond_wait(&pool->pooldestory,&pool->poolmutex);
pthread_mutex_unlock(&pool->poolmutex);
printf("开始释放资源\n");
if (pool == NULL)
{
return -1;
}
//关闭线程池
pool->shutdown = 1;
//销毁管理者线程,因为管理者线程在判断pool->shutdown == 1时会自动退出
pthread_join(pool->managerID,NULL);
//唤醒阻塞的消费者线程
printf("结束原有的线程池的线程\n");
int i = 0;
for ( i = 0; i < pool->livenum; i++)
{
pthread_cond_signal(&pool->notempty);
//唤醒所有阻塞等待分配任务的工作线程,因为他们发现pool->shutdown == 1 所以自动结束了
}
//释放互斥锁或者条件变量
pthread_mutex_destroy(&pool->poolmutex);
pthread_mutex_destroy(&pool->busymutex);
pthread_cond_destroy(&pool->notempty);
pthread_cond_destroy(&pool->notfull);
//释放内存
if (pool->thread_tID)
{
free(pool->thread_tID);
pool->thread_tID = NULL;
}
if (pool->taskQ)
{
free(pool->taskQ);
pool->taskQ = NULL;
}
if (pool)
{
free(pool);
pool = NULL;
}
return 0;
}
测试线程池使用
主函数编写
/*************************************************************************
# 文件名称 : 01_pthreads.c
# 作者 : 郑文杰
# 邮箱 : yanxin0517@126.com
# 创建时间 : 2021年08月07日 星期六 08时01分31秒
************************************************************************/
#include<stdio.h>
#include "pthreadpool.h"
#include <unistd.h>
#include <stdlib.h>
void func(void * arg)
{
int num = *(int *)arg;
printf("线程ID为%ld 正在工作,number = %d\n",pthread_self(),num);
sleep(3);
}
int main(int argc, const char *argv[])
{
//创建出一个线程池
pthreadpool * pool = init_pthreadPoolCreat(5,200,100);
if (pool == NULL)
{
printf("创建线程池失败\n");
return -1;
}
printf("线程池创建完毕\n");
//添加任务
int i = 0;
for ( i = 0; i < 200; i++)
{
int * num = (int *)malloc(4);
*num = i;//给任务函数传参
ThreadPoolAdd(pool,func,num);
}
pthreadPoolDestory(pool);
return 0;
}
测试结果
程序开始运行,线程池启动,5个线程启动
线程池检测加线程工作
注:任务队列检测到任务多线程不够用,自动添加线程,程序内部要求每次加2个
附加中途图片
线程池检测结束线程
注:当线程池发现任务队列中任务不需要这么多线程存活时,进行自动结束线程操作
销毁线程池
注:当线程池检测扫40秒内未发生任务的到来则销毁线程池,当然这一步如果在服务器并发上不会出现次步骤,按需求
结束总结
线程池总体就是这么个东西,需要自己去实现,重点时了解线程池的构成部分,每个人实现步骤和实现结构都是不一样的,没有固定的格式。所以可以根据自己的需求去修改一些东西。ok!如果帮到了你,点个赞吧!