Linux单Reactor多线程架构应选epoll+std::thread:epoll高效支持高并发I/O,std::thread承载业务工作线程;accept()须由唯一Reactor线程执行并入队connfd;工作线程需非阻塞读写、循环处理部分写、事件注册/注销回归Reactor线程;连接销毁需确保epoll移除与引用归零,超时由Reactor统一管理。

Reactor核心对象怎么选:epoll + std::thread 是当前 Linux 主流组合
在 Linux 上构建单 Reactor 多线程架构,epoll 是必须的 I/O 多路复用后端——它比 select 和 poll 更高效,支持数万连接且不随 fd 数量线性退化。Reactor 线程只负责事件分发,不处理业务逻辑,所以不能用 std::async 或协程替代线程池;必须用 std::thread(或封装好的线程池)承载工作线程。注意:kqueue 仅限 macOS/BSD,IOCP 属于 Windows,跨平台需求强时需抽象事件循环接口,但本架构默认聚焦 Linux 生产环境。
accept() 必须在 Reactor 线程完成,否则会触发 EMFILE 或惊群
多个线程共用同一个监听 socket 并调用 accept() 是常见错误。Linux 下即使使用 SO_REUSEPORT,也仅适用于多 Reactor 场景;单 Reactor 下必须由唯一 Reactor 线程执行 accept(),再把新建立的 connfd 通过线程安全队列(如 moodycamel::ConcurrentQueue 或带锁的 std::queue + std::mutex)投递给工作线程。否则会出现:
- 多个线程同时
accept()导致部分连接被丢弃(尤其高并发短连接) -
EMFILE错误频发(文件描述符泄漏或未及时关闭) - 新连接分配不均,某些工作线程空转,另一些过载
典型做法是 Reactor 线程收到 EPOLLIN 事件后立即 accept4(fd, ..., SOCK_NONBLOCK),设置非阻塞并立刻将 connfd 入队。
工作线程怎么安全读写 socket:避免 EAGAIN 和 EWOULDBLOCK 导致逻辑卡死
工作线程拿到 connfd 后,必须以非阻塞方式调用 read() 和 write()。若直接用阻塞 socket,一次 read() 可能永远卡住;若忽略返回值,EAGAIN/EWOULDBLOCK 会被当成真实错误关闭连接。正确处理方式:
立即学习“C++免费学习笔记(深入)”;
- 每次
read()后检查返回值:0表示对端关闭,-1且errno == EAGAIN || errno == EWOULDBLOCK表示暂无数据,应退出本次处理、等待下次事件 -
write()可能只写出部分数据,必须用循环 +send()的返回值推进偏移,或改用writev()减少系统调用次数 - 不要在工作线程里调用
epoll_ctl()—— 所有事件注册/注销必须回到 Reactor 线程执行,可通过管道或 eventfd 通知 Reactor 线程“这个 fd 需要重新关注可写事件”
连接生命周期管理容易漏掉的三个点
单 Reactor 多线程下,连接对象(如 Connection 类)通常跨线程持有,销毁时机极易出错:
- Reactor 线程负责关闭监听 socket 和初始
connfd的注册,但实际close(connfd)应由工作线程在完成读写后发起(否则可能 close 正在 read 的 fd) - 连接对象内存释放必须确保:Reactor 线程已从
epoll中删除该 fd,且所有工作线程对该对象的引用均已结束——推荐用std::shared_ptr+ 弱引用检测,或在工作线程处理完最后一个包后,通过回调通知 Reactor 线程执行最终清理 - 超时管理不能只靠工作线程计时器:Reactor 线程需维护一个最小堆(如
std::priority_queue)统一管理所有连接的读写超时,到时主动断连,避免工作线程因忙于计算而忽略心跳
真正难的不是启动几个线程,而是让连接在 Reactor 和 worker 之间流转时不泄漏、不竞争、不误关——多数 crash 和 fd 耗尽问题都出在这里。


















