Epoll+线程池

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,常见错误:EINVALmax ≤ 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_ntoainet_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。

  • 作用:将客户端 socketclient_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_responsepthread 线程入口函数void*返回值 +void*参数是线程函数标准格式),由 epoll 工作线程在监听到客户端可读事件后创建,专门处理单个客户端的读写请求。

变量说明

  • client_sockfd:客户端通信套接字,从线程参数arg传入(由Busines_accept的 accept 返回)。

  • buffer[1500]:接收缓冲区,大小 1500 字节(对应以太网 MTU=1500,避免分片,是网络编程的经典取值)。

  • recvlenrecv函数的返回值,记录实际读取的字节数。

  • 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_rctime可重入版本 ,多线程下安全(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==0recv返回 0 表示客户端主动关闭连接(FIN 包),此时需要:

  1. close(client_sockfd):关闭客户端套接字,释放文件描述符资源。

  2. 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);
}

核心作用:初始化线程同步的核心组件,是线程池安全的关键:

  1. pthread_mutex_t lock:互斥锁,保护线程池的共享资源(任务队列、线程数、忙碌数等),防止多线程并发修改导致数据竞争。

  2. pthread_cond_t not_full:条件变量,用于 ** 生产者(任务提交者)** 等待:当任务队列满时,生产者阻塞等待,直到队列有空闲位置被唤醒。

  3. 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;
    }
}

核心逻辑:这是线程安全的关键,分两步理解:

  1. 队列满判断cur(当前任务数)等于qmax(队列最大容量)时,队列已满。

  2. 条件变量等待:

    调用

    复制代码
    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;
}

该代码块理论上不会执行(前面已通过ifwhile处理了关闭逻辑),但保留作为兜底,防止逻辑漏洞。

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(&not_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为假(线程池已关闭),则:

  1. 存活线程数thread_alive减 1;

  2. 解锁互斥锁;

  3. 打印退出日志,调用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(需要强制销毁指定数量的线程),则:

  1. thread_exitcode减 1(消耗一个退出名额);

  2. 存活线程数thread_alive减 1;

  3. 解锁,打印日志,销毁线程。

作用:实现线程池动态缩容,根据负载调整线程数量。

(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)销毁线程,完成生命周期。

面试答题技巧
  1. 先讲模型,再讲细节:先说明这是「生产者 - 消费者模型的线程池消费者逻辑」,再逐行拆解代码。

  2. 重点突出核心考点pthread_cond_wait的 while 循环、环形队列实现、锁粒度优化、优雅关闭机制。

  3. 主动提出优化方案:解释完代码后,主动说明代码的问题和优化点,体现工程能力。

  4. 结合实际场景:线程池是 Nginx、Redis、Tomcat 等中间件的核心组件,说明你对开源技术的理解。

4、管理者

Manager.c线程池管理器:动态调整线程数量、任务队列监控、线程池销毁等

  1. 这段代码是动态线程池中 ** 管理者线程(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)
    {

只要线程池未关闭(shutdowntrue/ 非 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

拆解为两个条件(同时满足才会扩容):

  1. 负载条件cur>=alive-busy(等待的任务数 ≥ 空闲线程数) 忙碌率 >=70%(线程负载过高)

  2. 规模限制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

拆解为两个条件(同时满足才会缩容):

  1. 负载条件

    复制代码
    busy*2 < alive

    (忙碌线程数的 2 倍 < 存活线程数,即空闲线程过多,负载极低)

    • 等价于:空闲线程数 alive-busy > busy,空闲线程数 > 忙碌线程数,线程资源浪费
  2. 规模限制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 错误码含义
  • ESRCHerrno.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

相关推荐
史迪仔01122 小时前
[QML] Qt Quick Dialogs 模块使用指南
开发语言·前端·c++·qt
谭欣辰2 小时前
Floyd算法:动态规划解最短路径
c++·算法·图论
杨凯凡2 小时前
【019】IO/NIO 概念:Web 开发要掌握到什么程度
java·开发语言·nio
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 84. 柱状图中最大的矩形 | C++ 两次单调栈基础扫法
c++·算法·leetcode
季明洵2 小时前
Java基础---逻辑控制(上)
java·开发语言·循环结构·分支结构·顺序结构
小苗卷不动2 小时前
OJ刷题之栈和排序(中等)
c++
沫璃染墨2 小时前
重生之我要手写 C++ list:从底层结构到 const 迭代器与迭代器失效全解
开发语言·c++
paeamecium2 小时前
【PAT甲级真题】- Favorite Color Stripe (30)
数据结构·c++·算法·pat
练习时长一年2 小时前
xlsx文件下载异常问题
java·开发语言