C语言实现简易Linux线程池代码,应对TCP高并发
一、创建线程池时要解决的问题
线程池是为了处理一系列重复且高并发的任务而定义出来的对任务进行分配的数据类型。
线程池的核心是对任务进行分配,所以其必须包括任务以及工作线程以及分配的逻辑函数
- 应对高并发任务时,
- 1.主线程需要提前注册好一定数量的线程,并让其处于休眠状态。
- 2.当有新的任务到来时,唤醒一个线程,分配给他这个任务,由线程执行此任务
- 3.主线程继续等待下一个任务。
- 4.子线程完成任务后,再次进入休眠状态,等待下一个任务。
- 需要解决的问题:
- 如何向线程传递任务信息
- 任务如何在线程之间分配,线程之间的竞争机制
- 如何执行线程的有序退出
1.向线程传递任务信息
由于线程函数可以接收一个void*类型的参数,因此可以将任务信息打包成一个结构体,传递给子线程。
//1.以TCP连接下载为例,需要向子进程传递需要连接的客户端连接,以及其他任务信息,//可以使用此结构作为统一接口,后面如有修改直接增加即可
typedef struct task{
int clientFD; //客户端TCP套接字文件描述符
int cmd; //后续使用
char name[256];//后续使用
}task_t,*ptask_t;
//2.当任务数量大于子线程数列时,需要考虑使用一个数据结构,将任务信息存储起来,
//由于任务先进先出的性质,可以使用队列实现
//2.1队列节点定义
typedef struct node{
task_t data; //任务信息
struct node* pNext;//下一个node节点指针
}node_t,*pnode_t;
//2.2队列定义
typedef struct workQueue{
pnode_t pfront;
pnode_t prear;
int num; //队列中节点数量
}workQueue_t,*pworkQueue_t;
2.任务在子线程之间的分配
- 由于线程之间共享一块虚拟内存区域,因此需要考虑资源的竞争。
所有线程均从任务队列中拿任务,因此,要保证单个线程拿任务信息时不受其他线程的影响,因此需要在传入参数中加入pthread_mutex_t变量 - 当队列中无任务时,线程需要阻塞等待,因此考虑加入一个pthread_cond_t变量
- 因为对于所有子进程来说,以上两个变量都应该相同,子线程的传入参数时任务队列的指针,所以,修改上面的任务队列结构体
//任务队列结构体完整版
typedef struct workQueue{
pnode_t pfront;
pnode_t prear;
int num; //队列中节点数量
int exitFlag; //用于接收线程退出信号
pthread_mutex_t mutex;//线程锁
pthread_cond_t cond; //线程条件变量
pthread_cond_t maincond; //通知主线程的条件变量
}workQueue_t,*pworkQueue_t;
//1.任务队列的初始化
void initWQ(pworkQueue_t pqueue){
memset(pqueue,0,sizeof(workQueue_t));
pqueue->pfront = NULL;
pqueue->prear = NULL;
pqueue->num = 0;
pqueue->exitFlag = 0;
pthread_mutex_init(&queue->mutex,NULL);
pthread_cond_init(&queue->cond,NULL);
pthread_cond_init(&queue->maincond,NULL);
}
//2.队列中添加元素
int push(pworkQueue_t pqueue,pnode_t pnode){
pnode->pNext = NULL;
if(pqueue->pfront == NULL){
pqueue->pfront = pnode;
pqueue->prear = pnode;
}else{
pqueue->prear->pNext = pnode;
pqueue->prear = pnode;
}
pqueue->num ++;
return 0;
}
//3.获取队首元素、需要传入非空指针的pnode
int pop(pworkQueue_t pqueue,pnode_t pnode){
if(NULL == pqueue |NULL ==pnode NULL = pqueue->pfront) return -1;
//赋值传回参数
*pnode = *pqueue->pfront;
//记录要删除的地址
pnode_t tmp = pqueue->pfront;
//处理删除队列最后一个元素的情况
if(NULL = pqueue->pfront->pNext){
pqueue->pfront = NULL;
pqueue->prear = NULL;
pqueue->num --;
free(tmp);
return 0;
}
pqueue->pfront = queue->pfront->pNext;
pqueue->num --;
free(tmp);
return 0;
}
//4.释放队列空间
int freeWQ(pworkQueue_t pqueue){
if(NULL == pqueue) return -1;
if(pqueue->num == 0){
free(pqueue);
return 0;
}
pworkQueue_t pA = pqueue->pfront;
pworkQueue_t pB = pqueue->pfront;
while(pA != NULL && pB!= NULL){
free(pA);
pA = pB->pNext;
pB = pA;
}
return 0;
}
3.线程的有序退出
1.主要考虑当主线程接收到SIGINT信号后,如何保证子线程释放掉占用的资源、完成子线程相应的原子任务后再退出.
2.在子线程完成任务前,主线程不能退出
3.由于采用了锁的机智,需要保证子进程在退出时,将相应的锁释放掉,由主线程统一摧毁。
二、 线程池的创建步骤
创建线程池的相关函数
//1.定义线程池结构体
typedef struct Pool{
pthread_t* pthid; //动态数组存储线程id
int num; //线程池中线程的数量
short startFlag; //线程池状态标志位
workQueue queue; //任务队列
}Pool_t,*pPool_t;
//2.线程池的初始化
int initPool(pPool_t p,int num){
memset(p,0,sizeof(Pool_t);
pthid = (pPool_t)callloc(num,sizeof(Pool_t));
p->num = num;
p->startFlag = 0;
initWQ(&queue);
return 0;
}
//3.启动线程池
void* threadFunc(void* arg);
int startPool(pPool_t p){
if(pool->startFalg!= 0) return -1;
//启动线程
for(int i = 0;i<p->num;++i){
pthread_create(&p->pthid[i],NULL,threadFunc,&p->queue);
}
p->startFlag = 1;
return 0;
}
//4.线程函数
void cleanFunc(void* arg);
void dealClient(const pnode_t pnode);
void* threadFunc(void* arg){
//0.获取变量
pworkQueue_t pq = (pworkQueue)arg;
pthread_mutex_t* pmutex = &pq->mutex;
pthread_cond_t* pcond = &pq->cond;
pthread_cond_t* pmaincond = &pq->maincond;
//1.设置清理函数
pthread_cleanup_push(cleanFunc,pmutex);
//2.循环监听,获取客户端套接字
int ret = -1;
//演示用,未设置循环退出函数
while(1){
//2.0判断是否退出
if(1 == pq->exitFlag){
printf("child thread exit");
break;
}
//2.1加锁
pthread_mutex_lock(&pq->mutex);
//2.2获取队列元素
node_t node;
ret = pop(pq,&node);
//当任务队列没有任务时
if(-1 == ret) {
pthread_cond_signal(pmaincond);
pthread_cond_wait(pcond,pmutex);
}else{
//第一次取到了任务
pthread_mutex_unlock(pmutex);
dealClient(&node);
continue;
}
ret = pop(pq,&node);
//2.3解锁
pthread_mutex_unlock(pmutex);
if(-1 == ret) continue;
dealClient(&node);
}
//3.设置清理函数有效
pthread_cleanup_pop(1);
pthread_exit(NULL);
}
//定义清理函数
void cleanFunc(void* arg){
pthread_mutex_t* pmutex = (pthread_mutex_t*)arg;
pthread_mutex_unlock(pmutex);
pthread_exit(NULL);
}
//定义处理客户端的相关函数
void dealClient(pnode_t pnode){
printf("clientFD = %d\n",pnode->task.ClientFD);
}
二、线程池的退出
- 线程有两种方式进行退出:
- 主线程直接使用cancle函数
- 通过发送标志位,通知子线程退出。
由于直接cancle的方式需要考虑子线程函数中cancle点的位置,还有可能导致单个子线程的任务中断。因此考虑使用退出标志位的方式进行。
- 另外,如果主线程收到了SIGINT的信号退出,主线程的当前执行的任务会被打断、在信号处理函数中更改子线程的退出标志也不是很方便,因此,考虑使用两个进程、异地拉起同步的方式执行有序退出。
主线程测试代码:
#define TESTNUM 20
//设置接收退出信号用的进程间通信管道
int exitPipe[2] = -1;
//信号处理函数
void sigFunc(int num ){
printf("sig is coming\n");
int flag = 1;
write(exitPipe[1],&flag,sizeof(falg);
}
int main()
{
pool_t pool;
poolInit(&pool,TESTNUM);
//测试由于只接收管道的输入情况,因此使用非阻塞IO
pipe2(exitPipe,O_NONBLOCK);
//父进程负责监听SIGINT信号,写入管道
if(fork()){
close(exitPipe[0]);
signal(SIGINT,sigFunc);
wait(NULL);
printf("parent exit\n");
close(exitPipe[1]);
exit(0);
}
//子进程关闭管道写端
close(exitPipe[1]);
//模拟接收客户端套接字描述符
for(int i = 0 ;i<TESTNUM-10;++i){
pnode_t pnode = (pnode_t)calloc(1,sizeof(node_t));
pnode->task.clientFD = i;
push(&pool.queue,pnode);
}
printf("done allocating\n");
//打印任务队列中的套接字描述符情况
pnode_t cur = pool.queue.pfront;
while(cur!= NULL){
printf("cur value = %d\n",cur->task.clientFD);
cur= cur->pNext;
printf("cur addr = %p\n",cur);
}
//线程池开始工作,
poolStart(&pool);
pthread_mutex_t* pmutex = &pool.queue.mutex;
pthread_cond_t* pmaincond = &pool.queue.maincond;
pthread_cond_t* pcond = &pool.queue.cond;
//
int x = 20;//模拟新接入的客户端文件描述符
int flag = 0;//是否接受到管道数据
while(1){
flag = read(exitPipe[0],&flag,sizeof(flag));
//管道有数据写入
if(1 == flag){
pool.queue.exitFlag = 1; //队列中写入退出信号
pthread_cond_broadcast(pcond);//唤醒所有子线程
break;
}
//主线程锁,简单生产者消费者模型
pthread_mutex_lock(pmutex);
if(0 < pool.queue.num){ //任务队列中仍有任务
pthread_cond_wait(pmaincond,pmutex);
}
//任务队列中没有任务了,模拟新的客户端接入
pnode_t pnode = (pnode_t)calloc(1,sizeof(node_t));
pnode->task.clientFD = x++;
push(&pool.queue,pnode);
pthread_mutex_unlock(pmutex);
pthread_cond_signal(pcond); //由于此处只是创建了一个套接字,所以唤醒一个进程执行任务即可,
//pthread_cond_broadcast(pcond);
if(x>100){//大于100自动退出
pool.queue.exitFlag = 1;
pthread_cond_broadcast(pcond);
break;
}
}
//线程合并
for(int i= 0;i<10;++i){
printf("wait %d\n",i);
pthread_join(pool.thid[i],NULL);
}
//关闭管道文件描述符
close(exitPipe[0]);
return 0;
}
三、改进方向
- 1.接入客户端时,使用epoll来监控管道的文件描述符,以及服务器的文件描述符(是否有客户端接入)。
- 2.高并发情况下,考虑使用非阻塞模型+epoll 边缘触发提高服务器效率
- 3.管道read/write未做异常处理其他异常处理机制
- 4.服务器程序转化为守护进程