Reactor反应堆
在 Linux 环境中,reactor 指的是一种基于 I/O 多路复用的事件驱动设计模式,其核心思想是通过一个专门的组件(反应器)监听多个 I/O 事件(如网络连接、数据读写等),当事件发生时触发对应的回调函数进行处理,从而让单个进程或线程能高效处理大量并发连接,避免传统阻塞 I/O 中因等待而造成的资源浪费。它通常依赖 Linux 的 select、poll 或 epoll 等系统调用来实现事件监听,常见于高性能网络服务器开发,比如通过 libevent、libev 等库来快速构建基于 reactor 模式的应用,以应对高并发场景下的 I/O 处理需求。
在多进程线程中,write更简单。
LT:只要可写就持续通知,ET:仅在缓冲区从满变为不满时通知一次。
把一个sockfd托管给select,poll,epoll。原因是因为sockfd上事件没有就绪,用托管来提高效率。
默认sockfd新建的情况下,读事件默认不是就绪的,因为输入缓冲区没有数据,所以默认添加epoll。而写事件是就绪的,因为输出缓冲区本来就有空间,所以,在前期,我们可以直接写,但是到了后期输出缓冲区可能会满,那时才需要epoll监听。
对于写,我们直接发,发送条件不满足,开启写事件关心,后续剩余的数据,epoll会协助我自动进行发送。
补充细节:
1.如果我们开头直接对一个sockfd设置写关心,epoll就会立即就绪。
2.未来我全部发完了,就要对关闭这个fd写事件关心
3.如果写缓冲区没满,数据也发完了,我们就不用开启对写事件关心。
4.直接发,我怎么知道写入条件不满足了呢,errno是EAGAIN即可。
所以,对写事件关心最好是ET非阻塞。LT阻塞根本发挥不了作用,LT非阻塞与ET差不多。
以上是用单进程+epoll+非阻塞实现的,下面是加入多线程和多进程的思路。
主reactor线程+工作者线程池

#include <sys/epoll.h>
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>// 线程安全的任务队列
struct task_queue_t {// ... 使用互斥锁(pthread_mutex_t)和条件变量(pthread_cond_t)实现
};void* reactor_thread(void* arg) {int epoll_fd = epoll_create1(0);int listen_fd; // 已初始化的监听socketstruct epoll_event event, events[MAX_EVENTS];// 将listen_fd加入epollevent.events = EPOLLIN;event.data.fd = listen_fd;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event);while (1) {int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);for (int i = 0; i < nfds; ++i) {if (events[i].data.fd == listen_fd) {// 处理新连接int conn_fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK);event.events = EPOLLIN | EPOLLET; // 边缘触发模式event.data.fd = conn_fd;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &event);} else if (events[i].events & EPOLLIN) {// 数据可读int conn_fd = events[i].data.fd;char buffer[BUFFER_SIZE];ssize_t count = read(conn_fd, buffer, BUFFER_SIZE);if (count > 0) {// 封装任务,推送给工作者线程池task_t* task = create_task(conn_fd, buffer, count);task_queue_push(global_task_queue, task);}}// ... 处理 EPOLLOUT, EPOLLHUP 等事件}}
}void* worker_thread(void* arg) {while (1) {task_t* task = task_queue_pop(global_task_queue); // 阻塞等待任务process_business_logic(task); // 处理业务逻辑// 处理完成后,可能需要写回响应// write(task->fd, response, response_len); // 注意:非阻塞写,可能需要处理EAGAINclose(task->fd); // 或者将fd交还给Reactor管理free_task(task);}
}
多reactor线程主从模型

命名管道(FIFO)唤醒子线程
在 Linux 下的 Reactor 多线程模型中,用命名管道(FIFO)唤醒子线程是一种常见的线程间事件通知方式,核心思路是通过 FIFO 传递 “唤醒信号”,让子线程从阻塞等待状态进入工作状态,具体逻辑如下:
核心设计
主线程(Reactor 核心):负责通过
epoll/select监听网络 I/O 事件(如客户端连接、数据到达),同时也监听一个命名管道的读端。当有新的 I/O 事件发生(比如收到客户端数据),主线程会将事件对应的任务(如数据处理)封装起来,通过写入命名管道的写端,发送一个 “唤醒信号”(可以是简单的字节,甚至不需要具体数据,仅用 “有数据可读” 这个事件本身作为信号)。子线程(工作线程):每个子线程阻塞在命名管道的读端(通过
read系统调用),等待 “唤醒信号”。当主线程向 FIFO 写入数据时,子线程的read会返回,从而被唤醒,此时子线程从任务队列(通常是主线程与子线程共享的队列,如环形缓冲区或链表)中取出任务并处理。为什么用命名管道?
- 简单可靠:FIFO 是 Linux 原生的进程间通信(IPC)机制,支持阻塞读写,天然适合 “等待 - 唤醒” 场景。子线程阻塞在
read(FIFO)时不消耗 CPU,被唤醒时能快速响应。- 与 I/O 多路复用兼容:主线程可以将 FIFO 的读端文件描述符加入
epoll监听集合,当有子线程需要被唤醒时,通过写 FIFO 触发epoll事件,避免主线程额外的轮询开销。- 跨线程通信:虽然命名管道常用于进程间通信,但在同一进程的多线程间也完全可用(线程共享文件描述符表),且无需复杂的同步机制(仅需保证任务队列的线程安全)。
关键细节
- 任务队列:FIFO 仅用于 “唤醒”,实际任务数据需通过线程安全的队列(如加锁的链表、无锁队列)传递,避免通过 FIFO 传递大量数据(效率低)。
- 信号量替代?:也可以用信号量(semaphore)唤醒子线程,但 FIFO 的优势是能与主线程的
epoll逻辑整合(主线程可同时监听网络事件和 FIFO 唤醒事件),而信号量的唤醒无法直接纳入epoll监听。- 多子线程处理:若多个子线程同时监听同一个 FIFO,Linux 会采用 “惊群效应” 的调度策略(即只有一个子线程被唤醒处理任务),可减少锁竞争。
这种模式本质是 “主线程负责事件监听与分发,子线程负责任务处理”,通过 FIFO 实现轻量的唤醒机制,结合共享队列传递任务,平衡了 I/O 效率与多线程并发处理能力,适合高并发网络场景(如服务器处理大量客户端请求)。
用 evfd 实现子线程唤醒
核心思路:用
evfd替代 FIFO 实现子线程唤醒
主线程(Reactor 核心):主线程通过
epoll监听网络 I/O 事件(如客户端连接、数据到达),同时将evfd的文件描述符加入epoll监听集合。当有新任务需要分配给子线程时,主线程向evfd写入一个计数器值(如1),触发evfd的读事件,以此作为 “唤醒信号”。子线程(工作线程):子线程阻塞在
evfd的read操作上(等待信号)。当主线程写入数据后,read会返回写入的计数器值,子线程被唤醒,从共享任务队列中取出任务处理。处理完成后,子线程可重置evfd的计数器(通过read清零),再次进入阻塞等待状态。为什么
evfd比 FIFO 更适合?
轻量高效:
evfd是 Linux 2.6.22 引入的专用事件通知机制,仅用于线程 / 进程间的事件通知,不涉及磁盘 I/O(FIFO 可能依赖文件系统),操作更轻量,唤醒延迟更低。与
epoll无缝整合:evfd的文件描述符可直接加入epoll监听,支持边缘触发(ET)和水平触发(LT)模式,与 Reactor 模型中epoll管理网络事件的逻辑完全兼容,主线程可统一处理网络 I/O 和线程唤醒事件。信号携带简单信息:
evfd通过写入 64 位整数(计数器)传递信号,不仅能唤醒线程,还可附带简单信息(如任务数量),避免 FIFO 中 “仅用有无数据作为信号” 的局限性。例如,主线程写入3可表示 “有 3 个新任务”。避免惊群效应(可选):若多个子线程监听同一个
evfd,read操作会让所有线程被唤醒(惊群),但可通过为每个子线程分配独立evfd解决(主线程按需唤醒指定子线程);而 FIFO 的惊群行为较难控制。线程安全友好:
evfd的write操作是原子的,多线程同时写入时计数器会累加,无需额外加锁;而 FIFO 的write可能需要同步机制避免数据错乱。关键实现细节
初始化
evfd:主线程通过eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)创建evfd,EFD_NONBLOCK避免写入阻塞,EFD_CLOEXEC确保进程退出时自动关闭。任务队列与同步:
evfd仅负责唤醒,实际任务需通过线程安全队列(如加锁的链表、pthread_mutex保护的队列)传递,子线程被唤醒后从队列取任务。重置计数器:子线程处理完任务后,需通过
read(evfd, &cnt, 8)读取计数器(返回值即为写入的数值),将其清零,否则epoll会持续触发读事件(水平触发模式下)。总结
在 Reactor 多线程模型中,
evfd是比 FIFO 更优的唤醒选择:它更轻量、与epoll整合更自然、支持简单信息传递,且适合高并发场景下的线程间通知。实际开发中,libevent、libev等 Reactor 库也常使用evfd作为内部事件通知机制,替代传统的管道或信号量。
多Reactor线程自我举荐
在 Reactor 多线程模型中,“自我举荐”(也可理解为 “主动认领” 或 “竞争调度”)是一种去中心化的任务分配思路,核心是让子线程主动竞争处理任务,而非由主线程集中分配,以此减少主线程的调度压力,提升并发效率。
核心逻辑
主线程(Reactor 核心):仅负责监听网络 I/O 事件(如数据到达),当事件发生时,将任务(如连接对象、待处理数据)放入一个 全局共享的线程安全任务队列,不参与任务分配,也不主动唤醒特定子线程。
子线程(工作线程):所有子线程处于 “主动轮询” 或 “阻塞等待” 状态,不断检查任务队列是否有新任务。一旦发现任务,就通过 原子操作(如 CAS) 或 锁竞争 主动 “认领” 任务(即 “自我举荐” 处理该任务),拿到任务后独立处理,处理完成后继续等待下一个任务。
与 “主线程唤醒” 模式的区别
- 传统模式(如用 FIFO/evfd 唤醒):主线程分配任务后,主动通知某个子线程(或广播唤醒),子线程被动响应。
- 自我举荐模式:主线程只 “抛任务”,子线程主动 “抢任务”,无需主线程单独唤醒,减少了主线程的调度逻辑。
关键实现细节
任务队列的线程安全:必须使用支持原子操作的无锁队列(如
moodycamel::ConcurrentQueue)或带锁的队列(如pthread_mutex保护的链表),确保多个子线程同时抢任务时不会出现数据竞争。无锁队列更优,可避免锁竞争带来的性能损耗。子线程的等待策略:
- 若任务频繁,子线程可采用 自旋等待(循环检查队列,不放弃 CPU),减少上下文切换开销。
- 若任务稀疏,子线程可阻塞在 条件变量(pthread_cond) 上,当队列有任务时由主线程或其他子线程唤醒(此时仍保留轻微的 “通知” 逻辑,但核心是竞争处理)。
负载均衡:依赖子线程的竞争机制天然实现负载均衡 —— 空闲的子线程会更快抢到任务,避免某一子线程过载。
优势与适用场景
- 减少主线程压力:主线程无需维护子线程状态(如负载情况),也无需设计唤醒策略,逻辑更简单,适合高并发场景。
- 动态扩展性好:新增子线程时无需修改主线程逻辑,直接加入竞争即可,适合需要动态调整线程数的场景。
- 适合 CPU 密集型任务:任务处理耗时较长时,子线程主动竞争可避免主线程调度延迟导致的任务堆积。
潜在问题
- 竞争开销:若任务粒度极小(如简单数据解析),子线程抢任务的竞争(如 CAS 重试、锁争夺)可能抵消并行收益,反而降低效率。
- 缓存抖动:多个子线程频繁访问共享队列,可能导致 CPU 缓存失效(缓存一致性开销),尤其在多核环境下。
这种模式更接近 “工作窃取”(Work Stealing)的简化版,核心是通过去中心化的竞争机制提升调度灵活性,常见于高性能并发框架(如某些 RPC 服务器、游戏服务器的任务处理模块)。
多进程 Reactor
在 Linux 下,Reactor 模型也可扩展到多进程场景,核心思路是通过 进程间通信(IPC) 实现事件分发与任务协作,解决单进程资源限制(如 CPU 核心利用率、内存隔离),同时保持 Reactor 模式 “事件驱动、异步处理” 的特性。
多进程 Reactor 核心设计
主进程(Listener 角色):
- 负责监听核心网络事件(如
socket监听端口的新连接),通常通过epoll/select管理监听套接字(listen_fd)。- 不直接处理业务逻辑,而是当新连接到来时(
accept事件),通过 负载均衡策略(如轮询、最少连接数)将连接分配给子进程。子进程(Worker 角色):
- 每个子进程拥有独立的 Reactor 实例(即独立的
epoll句柄),负责处理分配给自己的连接的 I/O 事件(如数据读写、断开等)。- 子进程间相互隔离(独立地址空间),通过 IPC 与主进程或其他子进程通信(如需共享数据)。
关键技术点:连接如何分配给子进程?
多进程 Reactor 的核心难题是 如何将主进程接收的新连接传递给子进程,常见方案有 3 种:
1. 主进程
accept后传递文件描述符(最常用)
- 主进程通过
accept获取新连接的conn_fd,然后用sendmsg+SCM_RIGHTS机制将conn_fd发送到目标子进程(通过 Unix 域套接字unix socket实现 IPC)。- 子进程接收
conn_fd后,将其加入自己的epoll监听集合,后续由子进程独立处理该连接的所有 I/O 事件。- 优势:主进程集中管理连接接入,子进程专注处理业务,隔离性好;
- 注意:
sendmsg传递文件描述符需通过 Unix 域套接字,且需确保子进程提前与主进程建立通信通道。2. 子进程共享监听套接字(
SO_REUSEPORT)
- 主进程创建
listen_fd后,通过SO_REUSEPORT选项让多个子进程共享同一个监听端口(每个子进程独立bind+listen同一端口)。- 内核会自动将新连接负载均衡到不同子进程(避免 “惊群效应”),子进程直接
accept自己的连接,无需主进程转发。- 优势:省去 IPC 传递
conn_fd的开销,性能更高;子进程完全对等,可动态扩缩容;- 限制:需要 Linux 3.9+ 支持
SO_REUSEPORT,且子进程间无集中调度,适合无状态服务(如 HTTP 服务器)。3. 信号量 / 管道触发子进程
accept
- 主进程监听
listen_fd,当有新连接时,通过信号(signal)或管道(pipe)通知某个子进程,子进程被唤醒后执行accept获取连接。- 劣势:信号不可靠(可能丢失),管道唤醒效率低,且
accept需加锁避免冲突,现已较少使用。进程间通信(IPC)的选择
- Unix 域套接字:用于传递文件描述符(
SCM_RIGHTS)或业务数据,可靠且高效,是多进程 Reactor 的首选。- 共享内存:适合需要高频读写共享数据的场景(如全局配置),需配合信号量(
semaphore)或互斥锁(futex)保证同步。- 消息队列 / 命名管道(FIFO):适合低频率的事件通知(如子进程状态上报),但效率低于 Unix 域套接字。
优势与适用场景
- 资源隔离:子进程崩溃不影响主进程和其他子进程,提高系统稳定性(如 Nginx 多进程模型)。
- 利用多核:通过多进程充分利用 CPU 多核,避免单进程的线程数限制。
- 适合无状态服务:如 Web 服务器、反向代理,子进程可独立处理请求,无需频繁通信。
注意事项
- 内存隔离成本:进程间数据不共享,需通过 IPC 传递信息,增加通信开销,不适合高频数据交互场景。
- 连接迁移困难:连接一旦分配给子进程,很难迁移到其他进程(需复杂的状态同步),适合短连接服务。
- 进程管理:需主进程监控子进程状态(如
waitpid),在子进程崩溃时自动重启,保证服务可用性。典型例子是 Nginx 的多进程模型:主进程负责管理监听端口和子进程,子进程通过
SO_REUSEPORT共享监听,独立处理连接,既利用多核又保证隔离性,是多进程 Reactor 在高性能服务器中的经典实践。
尾声:
历时许久,我的主线课程终于在今日正式收官。 这段学习旅程算不上一帆风顺——去年暑假的中途暂停,让原本的节奏被打乱,课程也一度搁置。好在没有彻底放弃,兜兜转转,还是咬牙把剩下的内容逐一攻克。更具挑战的是,课程要求一课对应一篇博客总结,每一次输出都需要复盘知识点、梳理思路,过程虽繁琐,却也让我对知识的掌握更扎实。 回头看,这份成果离不开机构老师们的专业指导,更要感谢始终没停下脚步的自己。那些坚持的日子,终究都变成了成长的勋章。

