epoll+多线程服务程序“惊群”

大家都知道,多个accept已经不会出现“惊群”现象了,但是多个epoll还是会出现惊群。

一下服务器程序是多线程的惊群现象。

#include <stdio.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>


int sockfd;
 
int pthread_handle_message(void)
{
   struct sockaddr_in cli_addr;
      int nfds=0;
   int newsockfd=0;
   int clilen=0;
   struct epoll_event ev;
    int fd_epoll=0;
       struct epoll_event events[20];
 
   printf(" come in pthread !!\n");
   
   fd_epoll=epoll_create(256);
 if(fd_epoll<0)
 {  
    printf("pthread create epoll failed!!!\n");
 }
 printf("pthread create epoll success!!!\n");

 ev.data.fd=sockfd;
 ev.events= EPOLLIN | EPOLLET;     
 epoll_ctl(fd_epoll,EPOLL_CTL_ADD,sockfd,&ev);
      printf(" add epoll success!!!\n");
   
   clilen = sizeof(cli_addr);
   
      printf("add epool event!\n");
      
      nfds=epoll_wait(fd_epoll,events,20,-1);
      printf("pthread[%d] wake up!\n", pthread_self());
      
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if(newsockfd==-1 && errno==EAGAIN)
{
    printf("no client connect, continue!!!\n");
    pthread_exit(NULL);
}
printf(" pthread[%d] accept socket !!\n", pthread_self());
  pthread_exit(NULL);
   
 }
 static void sig_handler(const int sig) {
    printf("Signal handled: %s.\n", strsignal(sig));
    exit(0);
}
int main(int argc, char *argv[])
{


 int newsockfd;


 char buffer[2560000];
 struct sockaddr_in serv_addr;
 int n;
 int flags=0;
 int i;
 int pid=0;


 signal(SIGINT, sig_handler);
 signal(SIGTERM, sig_handler);
 sockfd = socket(AF_INET, SOCK_STREAM, 0);
 if (sockfd < 0)
 {
  printf("opening socket error! \n");
  exit(-1);
 }
#if 1
 flags = fcntl(sockfd, F_GETFL, 0); 
 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
#endif  
 bzero((char *)&serv_addr, sizeof(serv_addr));
 serv_addr.sin_family = AF_INET;
 serv_addr.sin_addr.s_addr = INADDR_ANY;
 serv_addr.sin_port = htons(55560);
 if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
 {
  printf("socket binding error! \n");
  exit(-1);
 }


 printf("listen socket\n");
 listen(sockfd, 5);


  pthread_attr_t attr;
  pthread_t threadId;
                


  /*初始化属性值,均设为默认值*/ 
  pthread_attr_init(&attr); 
  pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); 
  /* 设置线程为分离属性*/ 
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  for(i=0; i < 3; i++)
  {
      if(pthread_create(&threadId,&attr,pthread_handle_message,NULL))
      { 
          perror("pthread_creat error!"); 
          exit(-1); 
      } 
  }
 
  while(1)
  {
      sleep(100);
  }
  close(sockfd); 
 
}
### 什么是 epoll 现象? 在 Linux 中,`epoll` 是一种高效的 I/O 多路复用机制,用于监控多个文件描述符上的事件。当 `epoll_wait` 被唤醒时,如果多个线程或进程都在等待同一个文件描述符的事件,则可能会引发所谓的 **现象 (Thundering Herd Problem)** 。具体来说,现象是指在一个事件发生时,所有正在阻塞于该事件的线程都会被唤醒,但实际上只有一个线程能够真正处理这个事件[^1]。 --- ### 现象的原因 #### 1. 文件描述符共享 在多进程或多线程环境中,多个工作单元可能共享同一个监听套接字(listen socket)。当有新的连接到达时,操作系统会通知所有的监听者,而实际上只需要其中一个去接受新连接即可。其他被唤醒的工作单元最终又重新进入休眠状态,这浪费了大量的 CPU 时间和资源[^2]。 #### 2. 唤醒机制 传统的 `select` 和早期版本的 `poll` 都存在类似的问题。虽然 `epoll` 设计上更高效,但如果未正确配置其行为模式或者使用不当,仍然会出现这种情况。例如,在水平触发模式 (`EPOLLET`) 下,如果没有采取额外措施防止重复唤醒,也可能导致现象的发生[^3]。 --- ### 解决方案 以下是几种常见的解决方法: #### 方法一:引入 WQ_FLAG_EXCLUSIVE 标记 通过修改内核代码,在调用 `ep_poll` 函数时为其设置 `WQ_FLAG_EXCLUSIVE` 标志位,可以有效减少不必要的线程唤醒次数。此标志告诉内核只应该唤醒一个合适的消费者来处理特定类型的事件,从而显著缓解甚至完全消除效应的影响。 ```c // 修改 ep_poll 的实现逻辑以支持独占队列标志 struct wake_q_head wqh; wake_q_init(&wqh); list_add_tail(&wait->entry, &wqh.head); __wake_up_common_lock(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key, int wake_flags | WQ_FLAG_EXCLUSIVE); ``` #### 方法二:采用 ET 边沿触发模式 相比默认使用的 LT (Level Triggered)模式,ET (Edge Triggered)模式更适合高性能场景下的应用开发。它要求应用程序显式读取完数据后再继续接收后续的通知消息,因此减少了频繁轮询带来的开销并间接抑制了潜在的风险。 需要注意的是,切换到 ET 模式后需要特别注意边界条件管理,比如部分读写操作可能导致遗漏某些更新信号等问题。 #### 方法三:单进程/单线程负责监听 另一种简单粗暴的办法就是让整个系统里唯一的一个工作者专门承担起监听职责,其余成员则专注于业务计算任务本身。这样做的好处在于彻底规避掉了因竞争同一资源而导致的各种麻烦事;不过缺点也很明显——扩展性和灵活性较差些。 对于 Nginx 来说,默认情况下它的 Worker Processes 并不会直接参与到 Listen Socket 上面来的 Read Event 监听当中去,而是借助 Master Process 统筹安排好了之后才分发给对应的子实例执行实际的任务流程。这样的架构设计很好地兼顾到了效率与稳定性两方面的需求。 #### 方法四:加锁控制访问权限 还可以考虑利用互斥量或者其他同步原语强制规定任何时候最多只能有一个实体有权尝试获取来自某个指定 File Descriptor 的最新动态变化情况报告。尽管这种方法理论上可行,但在实践中往往因为增加了额外的协调成本反而得不偿失。 --- ### 总结 综上所述,针对 Linux 系统中的 Epoll 现象提供了多种有效的应对策略。其中既包括底层技术改进层面的内容(如启用 Exclusive Wakeup),也有高层次框架调整方面的建议(如改用 Edge Trigged Mode 或限定单一 Listener)。开发者应当依据具体的项目需求权衡利弊选取最恰当的一种方式加以实施。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值