Epoll+线程池
这是一个基于 Linux C 语言 实现的高并发网络服务器项目,核心采用 epoll I/O 多路复用 + 线程池 + 生产者 - 消费者模型 架构,用于处理大量客户端并发连接。
一、核心网络 I/O 模块(epoll 事件驱动)
1、epoll初始化(Epoll_initliazer.c)
Epoll_initializer.cepoll 实例初始化、事件结构体配置、文件描述符(fd)注册等基础操作
代码解释:
(1)初始化 Epoll 事件结构体
struct epoll_event node; // 定义 Epoll 事件结构体(核心数据结构)
node.data.fd = sockfd; // 将监听fd存入事件数据域(epoll_event.data 用于存储用户数据,此处存fd)
node.events = EPOLLIN | EPOLLET; // 事件类型:可读(EPOLLIN) + 边缘触发(EPOLLET)
(2)epoll_event结构体:
struct epoll_event {
uint32_t events; // 监控的事件类型(如 EPOLLIN/EPOLLET 等)
epoll_data_t data; // 用户数据联合体,可存fd/指针等
};
EPOLLIN :监控文件描述符可读事件(客户端发数据、监听 fd 有新连接均触发)。
EPOLLET :设置边缘触发模式(Epoll 核心特性,下文详解)。

(3)创建 Epoll 实例
if((epfd=epoll_create(max))==-1){ // 调用系统调用创建 Epoll 实例
perror("Epoll_init create epoll failed"); // 失败打印错误信息
exit(0); // 异常退出
}
epoll_create(max):
-
内核层面创建 Epoll 实例,返回一个 Epoll 专用文件描述符(
epfd)。 -
内核内部会初始化红黑树 (存储所有监控的 fd)和就绪链表(存储已就绪的事件)。
-
失败返回
-1,常见错误:EINVAL(max ≤ 0)、ENOMEM(内存不足)。
epfd 的生命周期 :使用完必须调用 close(epfd) 释放,否则会导致文件描述符泄漏
(4)注册监控事件(添加 fd 到 Epoll)
if((epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&node))==-1){ // 向 Epoll 实例添加监控
perror("Epoll_init first set sockfd failed");
exit(0);
}

epoll_ctl(epfd, op, fd, event):Epoll 控制接口,负责增 / 删 / 改监控的 fd。
-
epfd:Epoll 实例 fd。 -
op:操作类型,EPOLL_CTL_ADD表示添加监控。 -
fd:要监控的文件描述符(此处为监听sockfd)。 -
event:关联的事件结构体(包含事件类型和用户数据)。
失败场景 :EEXIST(fd 已添加)、EINVAL(参数非法)、ENOMEM(内存不足)。
(5)初始化成功提示与返回
printf("Epoll initializer succuss epfd=%d\n",epfd); // 打印成功信息与 Epoll 实例 fd
return 0; // 函数执行成功返回 0
2、epoll循环监听(Epoll_listen.c)
这段代码是基于 Epoll + 线程池 实现的高并发 TCP 服务器核心监听模块,是 Linux 下高性能网络服务的经典架构。
(1)监听服务端的接受连接的套接字
(2)用来给客户端收发信息的套接字
Epoll_listen.cepoll 事件循环(epoll_wait),监听所有套接字的可读 / 可写事件,分发任务

代码解释:
(1)函数与头文件
#include<server.h>
int Epoll_listen(int sockfd,thread_pool_t*ptr,int max)
#include<server.h>:自定义头文件,包含线程池 (thread_pool_t)、任务 (task_t)、业务函数 (Busines_accept/Busines_response) 等自定义结构体和函数声明。
函数Epoll_listen:服务器核心监听函数,3 个参数含义:
-
sockfd:服务器监听 socket(listen()后的 fd),用于接收新客户端连接。 -
ptr:线程池指针,用于向线程池提交任务(生产者角色)。 -
max:epoll 单次epoll_wait最多返回的就绪事件数(即ready_array数组大小)。
(2)局部变量定义
task_t bs;
int readynum;
struct epoll_event ready_array[max];
printf("Epoll listening Running ...\n");
int flag=0;
int eflag;
task_t bs:任务结构体,封装了业务函数指针 和函数参数 ,用于提交给线程池执行。
readynum:存储epoll_wait返回的就绪事件数量。
struct epoll_event ready_array[max]:就绪事件数组,用于存储epoll_wait返回的所有就绪的文件描述符事件。
flag:用于打印监听轮次,仅日志作用。
eflag:就绪事件数组的遍历下标。
(3)外层循环:持续监听
while(ptr->shutdown){
printf("Epoll 开始第%d轮监听..\n",++flag);
if((readynum=epoll_wait(epfd,ready_array,max,-1))==-1){
perror("Epoll_listen epoll_wait call failed");
}
// ... 事件处理
}
return -1;

while(ptr->shutdown):线程池的shutdown标志控制循环,shutdown=1时持续监听,shutdown=0时退出循环、关闭服务。
epoll_wait:epoll 核心系统调用,作用是阻塞等待就绪事件:
-
epfd:epoll 实例的文件描述符(由epoll_create创建,在server.h中封装在thread_pool_t或全局)。 -
ready_array:存储就绪事件的数组。 -
max:数组最大长度,即单次最多返回的事件数。 -
-1:超时时间,-1表示永久阻塞,直到有事件就绪才返回。 -
返回值
readynum:就绪事件的数量,-1表示调用失败(如被信号中断)。
return -1:循环退出后返回 - 1,仅作为函数返回值,实际服务退出时不会走到这里。
(4)内层循环:处理就绪事件
eflag=0;
while(readynum){
if(ready_array[eflag].data.fd==sockfd){
//添加连接任务
bs.busines=Busines_accept;
bs.arg=(void*)&sockfd;
Producer_add(ptr,bs);
printf("Producer (0x%x) add Accept bs success\n",(unsigned int )pthread_self());
}else{
//添加处理请求的任务
bs.busines=Busines_response;
bs.arg=(void*)&ready_array[eflag].data.fd;
Producer_add(ptr,bs);
printf("Producer (0x%x) add Response bs success\n",(unsigned int)pthread_self());
}
--readynum;
++eflag;
}
这是整个函数的核心逻辑,区分两类事件,分别提交不同任务给线程池:
(1) 事件分类逻辑
-
ready_array[eflag].data.fd==sockfd
-
该事件是监听 socket 的可读事件 ,表示有新客户端发起
connect(),需要执行accept()接收连接。 -
任务
bs的业务函数设为Busines_accept(自定义的连接处理函数,内部执行accept(),并将新客户端 fd 加入 epoll 监听),参数为监听sockfd。 -
调用
Producer_add(ptr,bs):向线程池的任务队列添加任务(生产者操作,线程池的消费者线程会取出任务执行)。
-
-
else
-
该事件是已连接客户端 socket 的可读事件 ,表示客户端发送了数据,需要执行
read()读取并处理请求、返回响应。 -
任务
bs的业务函数设为Busines_response(自定义的请求处理函数,内部执行read()、业务逻辑、write()响应),参数为客户端 fd。 -
同样调用
Producer_add提交任务给线程池。
-
(2)辅助逻辑
-
--readynum:每处理一个事件,就绪数减 1,直到所有事件处理完成。 -
++eflag:遍历就绪事件数组的下一个元素。 -
pthread_self():打印当前线程 ID,用于日志排查(该函数运行在主线程,即 epoll 监听线程)。
3、业务函数accept接收连接并加入epoll(Busines_accept.c)
Busines_accept.c处理新客户端连接(accept 系统调用),将新连接的 fd 添加到 epoll 监听
Busines_accept 是一个线程入口函数 (void* 返回值 + void* 参数是 pthread 线程函数的标准格式),专门负责处理客户端连接请求的 accept 操作,是高并发服务器中「监听线程 / 主线程」的核心逻辑。

代码解释
(1)变量说明:
-
server_sockfd:监听套接字(从线程参数arg传入,是主线程创建的监听 socket)。 -
client_sockfd:客户端连接套接字(accept 成功后返回,用于和客户端通信)。 -
struct epoll_event node:epoll 事件结构体,用于向 epoll 实例注册客户端 socket 的可读事件。 -
struct sockaddr_in clientaddr:存储客户端的 IP、端口等地址信息。 -
socklen_t addrlen:地址结构体长度,accept 的入参。 -
char cip[16]:存储客户端 IP 字符串(IPv4 地址最大 15 位 + 1 位 '\0',16 字节足够)。
(2)accept 客户端连接
addrlen=sizeof(clientaddr);
if((client_sockfd=accept(server_sockfd,(struct sockaddr*)&clientaddr,&addrlen))==-1){
perror("Business accept,accept call failed");
exit(0);
}
核心逻辑 :accept() 从监听套接字server_sockfd的已完成连接队列 中取出一个客户端连接,返回一个新的client_sockfd(用于后续和该客户端通信)。
参数说明:
-
server_sockfd:监听套接字,必须提前完成socket() -> bind() -> listen()三步。 -
(struct sockaddr*)&clientaddr:用于接收客户端的地址信息(IP、端口)。 -
&addrlen:入参是clientaddr的长度,出参是实际写入的长度。
错误处理 :accept 失败时用perror打印错误信息,exit(0)退出进程(实际生产中一般不会直接 exit,而是重试或记录日志)。
(3)客户端 IP 解析 + 首次响应
//首次响应
bzero(cip,16);
inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,cip,16);
FIRST_RESPONSE(client_sockfd,cip);
bzero(cip,16) :将cip数组清零,避免脏数据。(等价于memset(cip, 0, 16),bzero 是 POSIX 标准函数,部分系统推荐用 memset)
inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,cip,16):
-
作用:将网络字节序的二进制 IP 地址 (
clientaddr.sin_addr.s_addr)转换为点分十进制字符串 (如192.168.1.100),存入cip。 -
对比
inet_ntoa:inet_ntoa是不可重入函数(静态缓冲区),多线程下不安全;inet_ntop是线程安全的,高并发服务器必须用inet_ntop。
FIRST_RESPONSE(client_sockfd,cip) :自定义宏 / 函数,用于给客户端发送首次握手响应(比如登录验证、协议版本确认、欢迎信息等),是业务逻辑层的封装。
(4)向 epoll 注册客户端 socket 事件
//将clientsock设置监听
node.data.fd=client_sockfd;
node.events=EPOLLIN|EPOLLET;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,client_sockfd,&node)==-1){
perror("Busines accept,epoll_ctl call failed");
}
return NULL;
}
node.data.fd=client_sockfd:将事件关联的文件描述符设为客户端 socket,epoll 触发事件时会带回这个 fd。
node.events=EPOLLIN|EPOLLET:事件类型设置:
-
EPOLLIN:表示关注可读事件(客户端有数据发送过来时,epoll 会通知该 fd)。 -
EPOLLET:边缘触发(Edge Triggered) 模式,是高并发服务器的核心优化点(后面详细讲原理)。
epoll_ctl(epfd,EPOLL_CTL_ADD,client_sockfd,&node):
-
epfd:epoll 实例的文件描述符(全局变量,由主线程epoll_create()创建)。 -
EPOLL_CTL_ADD:操作类型,向 epfd 中添加一个需要监听的 fd。 -
作用:将客户端 socket
client_sockfd注册到 epoll 中,后续 epoll 会监控该 fd 的可读事件,一旦有数据就通知工作线程处理。
错误处理:epoll_ctl 失败时打印错误,不退出进程(避免单个客户端注册失败导致整个服务挂掉)。
return NULL:线程函数返回,该线程的生命周期结束(如果是「一连接一线程」模型,线程会退出;如果是「监听线程单独处理 accept」,线程会循环 accept)。
4、业务函数处理客户端的数据(Busines_response.c)
Busines_response.c业务逻辑处理:读取客户端请求、执行业务计算、构造并发送响应数据
这段代码是Linux 高并发 epoll 服务器的业务处理线程入口函数 ,负责客户端请求读取、协议解析、业务响应、连接销毁的全流程,是服务器业务逻辑的核心。
接受数据
(1)判断数据若是请求时间则返回时间
(2)判断数据若不是请求时间,则原样返回

代码解释
(1)函数与变量定义
#include<server.h>
void* Busines_response(void*arg)
{
int client_sockfd;
client_sockfd=*(int*)arg;
char buffer[1500];
int recvlen;
time_t tp;
char tm[1024];
char * msg="Please try again\n";
bzero(buffer,sizeof(buffer));
函数定位 :Busines_response是pthread 线程入口函数 (void*返回值 +void*参数是线程函数标准格式),由 epoll 工作线程在监听到客户端可读事件后创建,专门处理单个客户端的读写请求。
变量说明:
-
client_sockfd:客户端通信套接字,从线程参数arg传入(由Busines_accept的 accept 返回)。 -
buffer[1500]:接收缓冲区,大小 1500 字节(对应以太网 MTU=1500,避免分片,是网络编程的经典取值)。 -
recvlen:recv函数的返回值,记录实际读取的字节数。 -
time_t tp:时间戳类型,存储系统当前时间。 -
tm[1024]:存储格式化后的本地时间字符串。 -
msg:错误响应字符串,当客户端请求不合法时返回。 -
bzero(buffer,sizeof(buffer)):清空接收缓冲区,避免脏数据(等价于memset(buffer, 0, sizeof(buffer)))。
(2)循环读取客户端请求(核心 IO 逻辑)
//读取协议 协议解析
while((recvlen=recv(client_sockfd,buffer,sizeof(buffer),MSG_DONTWAIT))>0){
buffer[strlen(buffer)-1]='\0';
//处理请求并响应
if((strcmp(buffer,"localtime"))==0){
bzero(tm,sizeof(tm));
tp=time(NULL);
ctime_r(&tp,tm);
send(client_sockfd,tm,strlen(tm),MSG_NOSIGNAL);
}else{
send(client_sockfd,msg,strlen(msg),MSG_NOSIGNAL);
}
}
1)recv函数详解
recvlen=recv(client_sockfd,buffer,sizeof(buffer),MSG_DONTWAIT)
函数作用 :从客户端套接字client_sockfd读取数据到buffer,返回实际读取的字节数。
关键参数MSG_DONTWAIT:
-
作用:将本次
recv操作设为非阻塞 ,如果缓冲区无数据,立即返回-1,错误码EAGAIN/EWOULDBLOCK,不会阻塞线程。 -
适配场景:代码配合
epoll ET(边缘触发)模式使用,ET 模式要求必须一次性读完所有数据,因此用非阻塞recv循环读取,直到无数据可读。
循环条件>0 :只有当recv成功读取到数据(返回值 > 0)时,才进入循环处理请求。
2)缓冲区处理
buffer[strlen(buffer)-1]='\0';
作用 :将客户端发送的字符串末尾的换行符(\n)替换为字符串结束符\0,确保strcmp能正确比较字符串。
潜在风险 :如果客户端发送的字符串不以\n结尾,strlen(buffer)-1会越界访问内存,导致程序崩溃(面试高频坑点)。
3)业务逻辑:协议解析与响应
代码实现了一个极简的应用层协议:
-
客户端发送
localtime字符串 → 服务器返回当前系统本地时间 -
客户端发送其他内容 → 服务器返回
Please try again\n错误提示
① 时间请求处理逻辑
if((strcmp(buffer,"localtime"))==0){
bzero(tm,sizeof(tm));
tp=time(NULL);
ctime_r(&tp,tm);
send(client_sockfd,tm,strlen(tm),MSG_NOSIGNAL);
}
time(NULL):获取系统当前时间戳(从 1970-01-01 00:00:00 UTC 到现在的秒数),存入tp。
ctime_r(&tp,tm):
-
作用:将时间戳转换为可读的本地时间字符串 (格式:
Wed Jun 30 21:49:08 1993\n),存入tm。 -
线程安全:
ctime_r是ctime的可重入版本 ,多线程下安全(ctime使用静态缓冲区,多线程会出现数据竞争,高并发服务器必须用ctime_r)。
send(client_sockfd,tm,strlen(tm),MSG_NOSIGNAL):
-
作用:将时间字符串发送给客户端。
-
关键参数
MSG_NOSIGNAL
- 作用:禁止
SIGPIPE信号。当向一个已经关闭的套接字写数据时,系统会默认发送SIGPIPE信号,直接终止进程。MSG_NOSIGNAL会忽略该信号,让send返回错误,由程序自行处理,避免服务崩溃(高并发服务器必须加的参数)。
- 作用:禁止
② 错误请求处理逻辑
else{
send(client_sockfd,msg,strlen(msg),MSG_NOSIGNAL);
}
当客户端请求不是localtime时,返回错误提示Please try again\n,同样使用MSG_NOSIGNAL避免SIGPIPE。
(3)连接状态处理与资源释放
if(recvlen==0){
//client exit
close(client_sockfd);
epoll_ctl(epfd,EPOLL_CTL_DEL,client_sockfd,NULL);//取消监听
}else{
//recv failed
}
sleep(10);
return NULL;
}
1)客户端正常断开连接
if(recvlen==0){
close(client_sockfd);
epoll_ctl(epfd,EPOLL_CTL_DEL,client_sockfd,NULL);
}
recvlen==0:recv返回 0 表示客户端主动关闭连接(FIN 包),此时需要:
-
close(client_sockfd):关闭客户端套接字,释放文件描述符资源。 -
epoll_ctl(epfd,EPOLL_CTL_DEL,client_sockfd,NULL):从 epoll 实例中删除该套接字的监听,避免 epoll 继续监控已关闭的 fd,产生无效事件。
epfd:全局 epoll 实例文件描述符,由主线程epoll_create创建。
2)recv失败处理
else{
//recv failed
}
recvlen<0:表示recv调用失败,常见错误码:
-
EAGAIN/EWOULDBLOCK:非阻塞模式下无数据可读,属于正常情况,循环退出。 -
ECONNRESET:客户端强制断开连接(如拔网线、进程崩溃),需要关闭套接字并从 epoll 删除。 -
EINTR:系统调用被信号中断,可重试recv。
代码此处仅做了注释,未做错误处理,是典型的待优化点(面试高频考点)。
3)sleep(10)与线程退出
sleep(10);
return NULL;
sleep(10):线程退出前休眠 10 秒(实际生产中无意义,大概率是调试遗留代码,面试可指出问题)。
return NULL:线程函数返回,线程生命周期结束,资源由系统回收。
二、线程池与任务调度模块
1、线程池创建(Pool_create)
Pool_create.c线程池创建、初始化:创建工作线程、初始化任务队列、互斥锁 / 条件变量

(1)函数定义与基础结构
#include<server.h>
thread_pool_t *Pool_create(int tmax,int tmin,int qmax){
thread_pool_t *ptr=NULL;
函数功能:创建并初始化一个线程池,返回线程池句柄。
参数说明:
-
tmax:线程池最大线程数 -
tmin:线程池最小(核心)线程数 -
qmax:任务队列最大容量
thread_pool_t:自定义的线程池结构体,是整个线程池的核心数据结构,我们先补全它的典型定义(代码中未展示,但必须理解):
typedef struct task{
void (*func)(void*); // 任务函数指针
void *arg; // 任务参数
}task_t;
typedef struct thread_pool{
pthread_mutex_t lock; // 互斥锁,保护线程池共享资源
pthread_cond_t not_full; // 条件变量:任务队列未满(生产者等待)
pthread_cond_t not_empty; // 条件变量:任务队列非空(消费者等待)
pthread_t *ctids; // 工作线程ID数组
pthread_t mtid; // 管理者线程ID
task_t *list; // 任务队列(环形队列实现)
int front; // 队列头
int rear; // 队列尾
int cur; // 当前队列任务数
int qmax; // 队列最大容量
int thread_max; // 最大线程数
int thread_min; // 最小线程数
int thread_alive; // 存活线程数
int thread_busy; // 忙碌线程数
int shutdown; // 线程池是否关闭(1=关闭,0=运行)
int thread_exitcode; // 线程退出标志
}thread_pool_t;
(2)线程池结构体内存分配
if((ptr=(thread_pool_t*)malloc(sizeof(thread_pool_t)))==NULL){
perror("Pool_create malloc ptr failed");
}
作用:为线程池结构体分配堆内存,失败则打印错误信息。
注意:perror会打印malloc失败的系统原因(如内存不足),是 C 语言排查系统调用错误的标准方式。
(3)线程池基础参数初始化
ptr->shutdown=true;
ptr->thread_max=tmax;
ptr->thread_min=tmin;
ptr->thread_alive=0;
ptr->thread_busy=0;
ptr->thread_exitcode=0;
-
关键说明:
-
shutdown=true:先标记线程池为关闭状态 ,直到所有初始化完成、线程启动后,再改为false,避免未初始化完成就有任务进入。 -
thread_alive:当前存活的工作线程数,初始为 0,后续创建线程时递增。 -
thread_busy:当前忙碌的工作线程数,用于管理者线程动态扩缩容。
-
(4)任务队列内存分配
if((ptr->list=(task_t*)malloc(sizeof(task_t)*qmax))==NULL){
perror("Pool_Create malloc list failed");
}
ptr->front=0;
ptr->reat=0; // 代码笔误,正确应为 rear=0
ptr->cur=0;
ptr->qmax=qmax;
作用:为环形任务队列分配内存,初始化队列头尾指针和当前任务数。
环形队列原理:用数组模拟队列,front指向队头(取任务位置),rear指向队尾(放任务位置),cur记录当前任务数,避免数组越界,实现高效的任务存取。
笔误修正:reat是拼写错误,正确成员名应为rear,否则编译报错。
(5)工作线程 ID 数组分配
if((ptr->ctids=(pthread_t*)malloc(sizeof(pthread_t)*tmax))==NULL){
perror("Pool_create malloc ctids failed");
}
bzero(ptr->ctids,sizeof(pthread_t)*tmax);
作用:为最大线程数分配线程 ID 数组,用bzero清零内存(也可使用memset),避免脏数据。
pthread_t:POSIX 线程库定义的线程 ID 类型,用于标识线程。
(6)互斥锁与条件变量初始化
if(pthread_mutex_init(&ptr->lock,NULL)!=0||pthread_cond_init(&ptr->not_full,NULL)!=0||pthread_cond_init(&ptr->not_empty,NULL)!=0){
printf("Pool_create init lock or cond failed");
exit(0);
}
核心作用:初始化线程同步的核心组件,是线程池安全的关键:
-
pthread_mutex_t lock:互斥锁,保护线程池的共享资源(任务队列、线程数、忙碌数等),防止多线程并发修改导致数据竞争。 -
pthread_cond_t not_full:条件变量,用于 ** 生产者(任务提交者)** 等待:当任务队列满时,生产者阻塞等待,直到队列有空闲位置被唤醒。 -
pthread_cond_t not_empty:条件变量,用于 ** 消费者(工作线程)** 等待:当任务队列为空时,工作线程阻塞等待,直到有新任务入队被唤醒。
错误处理:任意一个同步组件初始化失败,直接退出程序,因为线程池无法在无同步的情况下安全运行。
(7)启动核心(最小)工作线程
//create cusomter (代码笔误,正确应为 customer,即消费者线程)
int err;
for(int i=0;i<ptr->thread_min;i++){
if((err=pthread_create(&ptr->ctids[i],NULL,Customer,(void*)ptr))>0){
printf("Pool_create ,create customer error:%s\n",strerror(err));
}else{
++(ptr->thread_alive);
}
}
作用:创建tmin个核心工作线程(消费者线程),这些线程会一直运行,等待任务队列中的任务。
pthread_create参数说明:
-
&ptr->ctids[i]:线程 ID 存储位置 -
NULL:线程属性(默认属性) -
Customer:线程执行函数(工作线程的主循环,负责取任务、执行任务) -
(void*)ptr:传递线程池指针给工作线程,让线程能访问线程池资源
错误处理:用strerror(err)打印线程创建失败的具体原因(如资源不足),创建成功则递增thread_alive。
笔误修正:cusomter拼写错误,正确为customer。
(8)启动管理者线程
//create manager
if((err=pthread_create(&ptr->mtid,NULL,Manager,(void*)ptr))>0){
printf("Pool_create ,create manager error:%s\n",strerror(err));
}
作用:创建管理者线程(Manager 线程),这是动态线程池的核心:
-
管理者线程会定期(如每 0.5 秒)检查线程池状态:
-
若任务队列繁忙、空闲线程不足:扩容 ,创建新的工作线程(不超过
tmax) -
若任务队列空闲、空闲线程过多:缩容 ,销毁多余的工作线程(不低于
tmin)
-
-
实现线程池的动态扩缩容,平衡性能与资源占用。
2、生产者(producer_add)
这段代码是线程池中生产者向任务队列添加任务的核心逻辑,对应「生产者 - 消费者模型」中的生产者角色。
Producer.c生产者角色:epoll 监听到事件后,将任务封装并投递到任务队列

(1)函数定义与入口
int Producer_add(thread_pool_t*ptr,task_t bs)
{
功能:向线程池的任务队列中添加一个新任务。
参数:
-
ptr:指向线程池结构体的指针,操作目标线程池。 -
bs:待添加的任务(task_t类型,包含任务函数指针和参数)。
返回值 :int,成功返回0,失败返回-1。
(2)加锁保护共享资源
pthread_mutex_lock(&ptr->lock);
核心作用 :对线程池的共享资源(任务队列、队列状态等)加锁,保证操作的原子性。
必要性:多线程环境下,若不加锁,多个生产者同时修改队列会导致数据竞争(如队列索引错位、任务丢失)。
(3)检查线程池是否关闭
if(ptr->shutdown){
// 线程池已关闭,直接解锁并报错
pthread_mutex_unlock(&ptr->lock);
printf("Producer add busines error:shutdown close\n");
return -1;
}
逻辑 :若线程池处于关闭状态(shutdown=true),则无法添加新任务,直接释放锁并返回错误。
细节 :必须先解锁再返回,否则会导致死锁(持有锁却不释放,其他线程永远无法获取锁)。
(4)任务队列满时的阻塞等待
while(ptr->cur==ptr->qmax)//queue is full
{
pthread_cond_wait(&ptr->not_full,&ptr->lock);
if(!ptr->shutdown){
pthread_mutex_unlock(&ptr->lock);
printf("producer add busines error:shutdown close\n");
return -1;
}
}
核心逻辑:这是线程安全的关键,分两步理解:
-
队列满判断 :
cur(当前任务数)等于qmax(队列最大容量)时,队列已满。 -
条件变量等待:
调用
pthread_cond_wait(&ptr->not_full, &ptr->lock)-
原子操作 :该函数会自动释放互斥锁 并进入阻塞状态,等待被唤醒。
-
被唤醒时 :会重新持有互斥锁,然后返回继续执行。
-
为什么用while而非if?
-
防止虚假唤醒 (Spurious Wakeup):操作系统可能在无信号的情况下唤醒线程,用
while可以在唤醒后再次检查队列状态,若仍满则继续等待。 -
防止锁竞争:确保再次进入循环时,队列状态是最新的。
(5)执行任务入队操作
// add business
ptr->list[ptr->front]=bs; // 将任务放入队列头部位置
++(ptr->cur); // 当前任务数+1
ptr->front=(ptr->front+1)%ptr->qmax; // 移动队头指针(环形队列)
环形队列原理:
-
list是任务队列的数组首地址,front指向当前可存放任务的位置。 -
cur记录队列中当前的任务数量。 -
(front+1) % qmax:实现环形遍历,当front到达数组末尾时,回到数组头部,充分利用空间。
注意 :此处逻辑存在重大 Bug !标准的环形队列入队 应操作rear指针,而非front!
(6)解锁并唤醒消费者线程
pthread_mutex_unlock(&ptr->lock);
pthread_cond_signal(&ptr->not_empty);//signal send to customer
return 0;
解锁 :pthread_mutex_unlock,释放锁,允许其他线程操作共享资源。
唤醒消费者 :pthread_cond_signal(&ptr->not_empty),向等待队列非空的消费者线程发送信号,唤醒一个等待的线程去取任务执行。
返回 :任务入队成功,返回0。
(7)异常处理兜底
pthread_mutex_unlock(&ptr->lock);
printf("Producer add busines error:shutdown close\n");
return -1;
}
该代码块理论上不会执行(前面已通过if和while处理了关闭逻辑),但保留作为兜底,防止逻辑漏洞。
3、消费者
Customer.c
Linux 下基于生产者 - 消费者模型的线程池消费者线程(Worker 线程)核心逻辑,是高并发服务器(如前面的 epoll 服务器)的核心调度组件,负责从任务队列中取出任务并执行。
线程池里的【消费者工作线程】 作用:从任务队列取任务 → 执行任务
** 负责消费任务**
线程池里的工作线程,从队列取出任务并执行业务处理

(1)函数与结构体前置说明
#include<server.h>
void * Customer(void * arg)
{
thread_pool_t * ptr=(thread_pool_t*)arg;
task_t bs;
函数定位 :Customer是线程池工作线程(消费者线程)的入口函数,由线程池初始化时创建,生命周期与线程池绑定。
参数说明 :arg传入线程池结构体指针thread_pool_t*,线程通过该指针访问线程池的任务队列、锁、条件变量等共享资源。
(2)线程池主循环:消费者核心逻辑
while(ptr->shutdown){
pthread_mutex_lock(&ptr->lock);
while(ptr->cur==0){
pthread_cond_wait(&ptr->not_empty,&ptr->lock);
if(!ptr->shutdown){
--(ptr->thread_alive);
pthread_mutex_unlock(&ptr->lock);
printf("shutdown is false ,Customer 0x%x exit\n",(unsigned int)pthread_self());
pthread_exit(NULL);
}
if(ptr->thread_exitcode){
--(ptr->thread_exitcode);
--(ptr->thread_alive);
pthread_mutex_unlock(&ptr->lock);
printf("Customr 0x%x exiting\n",(unsigned int )pthread_self());
pthread_exit(NULL);
}
}
1)外层循环:while(ptr->shutdown)
- 作用:线程池运行状态的守护循环 ,只要
shutdown为真(线程池未关闭),线程就持续从任务队列取任务执行;当shutdown为假时,循环退出,线程准备销毁。
2)互斥锁加锁:pthread_mutex_lock(&ptr->lock)
- 作用:对线程池的共享资源(任务队列、计数器等)加锁,保证同一时间只有一个线程访问任务队列,避免并发竞争导致的数据错乱(如多个线程同时取同一个任务)。
3)任务队列空等待:while(ptr->cur==0) + pthread_cond_wait
-
while(ptr->cur==0):循环判断任务队列是否为空(cur是当前任务数),必须用 while 而非 if(面试高频考点),防止虚假唤醒。 -
pthread_cond_wait(&ptr->not_empty,&ptr->lock)-
作用:当任务队列为空时,消费者线程阻塞等待 ,释放持有的互斥锁,让生产者线程可以向队列添加任务;当生产者调用
pthread_cond_signal(¬_empty)唤醒时,线程重新获取锁,继续执行。 -
原子性:
pthread_cond_wait的「释放锁 + 阻塞等待」是原子操作,不会出现锁释放前生产者无法加锁的问题。
-
4)线程池关闭处理
if(!ptr->shutdown){
--(ptr->thread_alive);
pthread_mutex_unlock(&ptr->lock);
printf("shutdown is false ,Customer 0x%x exit\n",(unsigned int)pthread_self());
pthread_exit(NULL);
}
逻辑:线程被唤醒后,先检查shutdown标志:如果shutdown为假(线程池已关闭),则:
-
存活线程数
thread_alive减 1; -
解锁互斥锁;
-
打印退出日志,调用
pthread_exit(NULL)销毁线程。
5)强制线程退出处理
if(ptr->thread_exitcode){
--(ptr->thread_exitcode);
--(ptr->thread_alive);
pthread_mutex_unlock(&ptr->lock);
printf("Customr 0x%x exiting\n",(unsigned int )pthread_self());
pthread_exit(NULL);
}
逻辑:如果thread_exitcode>0(需要强制销毁指定数量的线程),则:
-
thread_exitcode减 1(消耗一个退出名额); -
存活线程数
thread_alive减 1; -
解锁,打印日志,销毁线程。
作用:实现线程池动态缩容,根据负载调整线程数量。
(3)从任务队列取任务并执行
//get busines exec
bs=ptr->list[ptr->reat];
--(ptr->cur);
ptr->reat=(ptr->reat+1)%ptr->qmax;
++(ptr->thread_busy);
pthread_mutex_unlock(&ptr->lock);
pthread_cond_signal(&ptr->not_full);//signal send ot producer
bs.busines(bs.arg);
1)环形队列取任务
-
bs=ptr->list[ptr->reat]:从队列头reat位置取出任务,存入task_t bs。 -
--(ptr->cur):当前任务数减 1(队列任务减少)。 -
ptr->reat=(ptr->reat+1)%ptr->qmax:环形队列头指针后移,取模实现循环,避免数组越界(面试高频考点:环形队列的实现)。 -
++(ptr->thread_busy):忙碌线程数加 1(线程开始执行任务)。
2)解锁与唤醒生产者
-
pthread_mutex_unlock(&ptr->lock):释放互斥锁,允许其他线程访问任务队列。 -
pthread_cond_signal(&ptr->not_full):向生产者发送队列未满 的信号,唤醒阻塞在not_full上的生产者线程(当队列满时,生产者会阻塞等待)。
3)执行任务
-
bs.busines(bs.arg):调用任务函数指针busines,传入任务参数arg,执行具体业务(如前面的Busines_accept/Busines_response)。 -
关键:任务执行在锁外,避免长时间占用锁导致其他线程阻塞,这是线程池的核心设计原则(锁仅保护任务队列,不保护业务逻辑)。
(4)任务执行完成后的状态更新
pthread_mutex_lock(&ptr->lock);
--(ptr->thread_busy);
pthread_mutex_unlock(&ptr->lock);
}
任务执行完成后,重新加锁,将忙碌线程数thread_busy减 1(线程回到空闲状态),然后解锁,回到主循环等待下一个任务。
(5)线程池关闭后的退出逻辑
printf("shutdown is false ,customer 0x%x exit\n",(unsigned int)pthread_self());
pthread_exit(NULL);
}
当外层while(ptr->shutdown)循环退出(shutdown为假),线程打印退出日志,调用pthread_exit(NULL)销毁线程,完成生命周期。
面试答题技巧
-
先讲模型,再讲细节:先说明这是「生产者 - 消费者模型的线程池消费者逻辑」,再逐行拆解代码。
-
重点突出核心考点 :
pthread_cond_wait的 while 循环、环形队列实现、锁粒度优化、优雅关闭机制。 -
主动提出优化方案:解释完代码后,主动说明代码的问题和优化点,体现工程能力。
-
结合实际场景:线程池是 Nginx、Redis、Tomcat 等中间件的核心组件,说明你对开源技术的理解。
4、管理者
Manager.c线程池管理器:动态调整线程数量、任务队列监控、线程池销毁等
-
这段代码是动态线程池中 ** 管理者线程(Manager Thread)** 的核心逻辑,作用是:
-
周期性监控线程池的运行状态(存活线程数、忙碌线程数、空闲线程数)
-
根据预设阈值自动扩容 (任务多、线程不够用)或缩容(任务少、线程空闲)
-
保证线程池始终在
thread_min(最小线程数)~thread_max(最大线程数)范围内高效运行
-

(1) 函数定义与入参
#include<server.h> // 自定义头文件,包含线程池结构体 thread_pool_t 的定义
void* Manager(void *arg)
{
thread_pool_t *ptr=(thread_pool_t*)arg;
Manager 是管理者线程的入口函数,符合 pthread_create 要求的 void* (*)(void*) 函数签名
入参 arg 是线程池结构体 thread_pool_t 的指针,通过强制类型转换获取线程池的所有状态信息
(2)变量定义
int alive,busy,cur;
int flag;
int add;
int err;
| 变量 | 正确作用 |
|---|---|
alive |
线程池当前存活的工作线程总数 |
busy |
当前正在处理任务的忙碌线程数 |
cur |
当前任务队列中的任务数量(等待处理的任务数) |
flag |
遍历线程 ID 数组 ctids 时的循环下标 |
add |
本次扩容新增的线程数计数 |
err |
预留错误码变量,代码中未使用 |
(3)管理者线程主循环
while(ptr->shutdown)
{
只要线程池未关闭(shutdown 为 true/ 非 0),管理者线程就持续运行,周期性监控状态
线程池销毁时会将 shutdown 置为 false,循环退出,管理者线程结束
(4)加锁读取线程池状态(线程安全)
pthread_mutex_lock(&ptr->lock);
alive=ptr->thread_alive;
busy=ptr->thread_busy;
cur=ptr->cur;
pthread_mutex_unlock(&ptr->lock);
核心原理 :线程池的共享状态(alive/busy/cur)必须加互斥锁 pthread_mutex_t 保护,防止多线程并发读写导致数据不一致
读取完成后立即解锁,避免长时间占用锁影响工作线程的任务处理
(5)打印线程池状态(调试 / 监控)
//判断扩容与缩减
sleep(1);
printf("thread pool status\n alive %d busy %d idel %d B/A %.2f%% A/M %.2f%%\n",
alive,busy,alive-busy,(double)busy/alive*100,(double)alive/ptr->thread_max*100);
sleep(1):管理者线程每 1 秒执行一次状态检查,避免空转消耗 CPU
打印字段说明:
-
alive:存活线程数 -
busy:忙碌线程数 -
idel:空闲线程数(=alive-busy) -
B/A:忙碌率(忙碌线程 / 存活线程,反映线程负载) -
A/M:线程池使用率(存活线程 / 最大线程数,反映线程池规模利用率)
(6)自动扩容逻辑(核心)
if((cur>=alive-busy||(double)busy/alive*100>=70)&&alive+ptr->thread_min<=ptr->thread_max){
//遍历ctids,查找可用的位置存储tid
for(flag=0,add=0;flag<ptr->thread_max&&add<ptr->thread_min;flag++){
if(ptr->ctids[flag]==0||!pthread_thread_alive(ptr->ctids[flag])){
pthread_create(&ptr->ctids[flag],NULL,Customer,(void*)ptr);
pthread_mutex_lock(&ptr->lock);
++(ptr->thread_alive);
pthread_mutex_unlock(&ptr->lock);
add++;
}
}
}
1)扩容触发条件
(cur>=alive-busy||(double)busy/alive*100>=70)&&alive*ptr->thread_min<=ptr->thread_max
拆解为两个条件(同时满足才会扩容):
-
负载条件 :
cur>=alive-busy(等待的任务数 ≥ 空闲线程数)或 忙碌率>=70%(线程负载过高) -
规模限制 :
alive * thread_min <= thread_max(扩容后不超过最大线程数thread_max,这里的写法是 "每次最多新增thread_min个线程",避免一次性扩容过多)
2)扩容执行逻辑
遍历线程 ID 数组 ctids(存储所有工作线程的 pthread_t),寻找空闲位置 (ctids[flag]==0,未被使用)或已死亡线程 (!pthread_thread_alive,线程异常退出)
找到可用位置后,调用 pthread_create 创建新的工作线程 (入口函数 Customer,即任务消费者线程)
新线程创建后,加锁更新 thread_alive(存活线程数 + 1),保证线程安全
add 计数新增线程数,最多新增 thread_min 个(避免一次性扩容过多)
(7)自动缩容逻辑(核心)
if(busy*2<alive&&alive-ptr->thread_min>=ptr->thread_min){
pthread_mutex_lock(&ptr->lock);
ptr->thread_exitcode=ptr->thread_min;//设置缩减值
pthread_mutex_unlock(&ptr->lock);
for(int i=0;i<ptr->thread_min;i++){
pthread_cond_signal(&ptr->not_empty);
}
}
sleep(1);
}
}
1)缩容触发条件
busy*2<alive&&alive-ptr->thread_min>=ptr->thread_min
拆解为两个条件(同时满足才会缩容):
-
负载条件
busy*2 < alive(忙碌线程数的 2 倍 < 存活线程数,即空闲线程过多,负载极低)
- 等价于:空闲线程数
alive-busy > busy,空闲线程数 > 忙碌线程数,线程资源浪费
- 等价于:空闲线程数
-
规模限制 :
alive - thread_min >= thread_min(缩容后不低于最小线程数thread_min,保证线程池有足够的基础线程处理任务)
2)缩容执行逻辑
加锁设置 thread_exitcode = thread_min:告诉工作线程 "本次需要退出 thread_min 个线程"
循环调用 pthread_cond_signal(&ptr->not_empty):唤醒阻塞在条件变量 not_empty 上的空闲工作线程
工作线程被唤醒后,会检查 thread_exitcode,如果需要退出则主动结束线程,实现缩容
最后 sleep(1),控制缩容节奏,避免频繁缩容
5、检查线程是否存活(if_thread_alive)
动态线程池里检查线程是否存活的核心工具函数
if_thread_alive.c线程健康检查:监控工作线程状态,处理线程异常退出、资源回收

(1)函数定义
#include<server.h>
int if_thread_alive(pthread_t tid){
函数名 if_thread_alive:判断线程是否存活
入参 pthread_t tid:要检查的线程 ID(pthread_t 是 POSIX 线程的线程标识类型)
返回值:int 类型,约定:
-
返回
0:线程不存活(已退出 / 不存在) -
返回
1:线程存活
(2)核心检测逻辑
int err;
err = pthread_kill(tid, 0);
pthread_kill 是 POSIX 线程库的标准函数,原型:
int pthread_kill(pthread_t thread, int sig);
核心用法:
-
当
sig = 0时,不发送任何实际信号,仅执行「线程存在性检查」 -
这是 Linux 下检测线程是否存活的经典、标准方法
执行逻辑:
-
向线程
tid发送空信号0,系统会检查该线程是否存在 -
函数返回值
err-
0:线程存在(信号发送成功) -
错误码:线程不存在 / 无权限(如
ESRCH)
-
(3)错误判断与返回
if(err == ESRCH){
return 0;
}
return 1;
1)ESRCH 错误码含义
-
ESRCH是errno.h中定义的标准错误码,全称:No such process -
在这里表示:指定的线程 ID 不存在(线程已退出)
(2)正确逻辑
-
如果
err == ESRCH→ 线程不存在 → 返回0(不存活) -
其他情况(包括
err == 0线程存活、err == EINVAL信号非法等)→ 返回1(存活)
三、入口与主流程
1、main函数
基于 Epoll + 线程池的高并发 Linux 服务器主程序
main.c程序入口:初始化服务器 socket、绑定端口、启动 epoll、创建线程池、启动事件循环

(1)头文件与宏定义
#include<server.h>
#define EPOLL_MAX 100000
server.h:这是自定义的服务器头文件,封装了所有网络、Epoll、线程池相关的函数声明(NET_INITIALIZER/Epoll_initializer/Pool_create/Epoll_listen等),是工程中常见的模块化封装方式。
EPOLL_MAX 100000:定义 Epoll 事件表的最大容量,即服务器最多同时管理 10 万个文件描述符,是高并发场景下的性能配置。
(2)网络初始化:NET_INITIALIZER(NULL,8080)
这是对socket服务器初始化流程的封装,等价于以下系统调用的组合:
// 底层伪代码
int NET_INITIALIZER(const char *ip, int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 1. 创建TCP套接字
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 2. 地址复用
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = ip ? inet_addr(ip) : INADDR_ANY; // 3. 绑定IP和端口
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 128); // 4. 监听,设置半连接队列长度
return sockfd;
}
参数说明:NULL代表绑定本机所有网卡 IP,8080是服务端口。
核心作用:完成 TCP 服务器的创建、绑定、监听,返回监听套接字server_sockfd。
(3)Epoll 初始化:Epoll_initializer(server_sockfd,EPOLL_MAX)
这是 Epoll 实例创建的封装,底层等价于:
int Epoll_initializer(int listenfd, int max_events) {
int epfd = epoll_create1(0); // 创建Epoll实例(epoll_ctl的操作句柄)
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 监听读事件 + 边缘触发模式
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); // 将监听套接字加入Epoll事件表
return epfd;
}
核心作用:创建 Epoll 实例,并将服务器的监听套接字加入事件表,用于后续的连接事件监听。
参数解析:100为最大线程数、10为最小线程数、1000为任务队列的最大长度。
核心作用:预先创建一组线程,避免高并发场景下频繁创建 / 销毁线程的开销,后续处理客户端请求时,直接将任务丢入线程池即可。
(4)Epoll 事件循环:Epoll_listen(server_sockfd,ptr,EPOLL_MAX)
这是服务器的核心事件循环,是 Epoll + 线程池的核心调度逻辑,伪代码如下:
void Epoll_listen(int listenfd, thread_pool_t *pool, int max_events) {
int epfd = Epoll_initializer(...); // 之前创建的Epoll实例
struct epoll_event events[max_events];
while(1) { // 事件循环,服务器永不退出
int nready = epoll_wait(epfd, events, max_events, -1); // 阻塞等待事件发生
for(int i=0; i<nready; i++) {
if(events[i].data.fd == listenfd) {
// 1. 监听套接字可读:有新连接到来
int clientfd = accept(listenfd, ...); // 接受新连接
// 将客户端套接字加入Epoll事件表,监听读事件
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else {
// 2. 客户端套接字可读:收到数据,将任务丢入线程池处理
add_task(pool, clientfd, handle_client_request);
}
}
}
}
核心作用:通过epoll_wait监听所有事件,新连接直接加入 Epoll,客户端请求则交由线程池处理,实现高并发 IO。
2、可执行文件SERVER
SERVER编译生成的可执行文件(二进制程序)
gcc *.c -I../include lmynet -lpthread -o SERVER
四、服务器头文件
server.h

五、网络封装
在一些判断错误以及连接后的首次响应 9个函数
mynet.c

mynet.h

六、so动态库
gcc -fPIC -c -I../include ***.c -o mynet.o
gcc -shared -o libmynet.so mynet.o
gcc *.c -I../include -L../include -lmynet -o main



