当前位置: 首页 > news >正文

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 传递 “唤醒信号”,让子线程从阻塞等待状态进入工作状态,具体逻辑如下:

核心设计

  1. 主线程(Reactor 核心):负责通过 epoll/select 监听网络 I/O 事件(如客户端连接、数据到达),同时也监听一个命名管道的读端。当有新的 I/O 事件发生(比如收到客户端数据),主线程会将事件对应的任务(如数据处理)封装起来,通过写入命名管道的写端,发送一个 “唤醒信号”(可以是简单的字节,甚至不需要具体数据,仅用 “有数据可读” 这个事件本身作为信号)。

  2. 子线程(工作线程):每个子线程阻塞在命名管道的读端(通过 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 实现子线程唤醒

  1. 主线程(Reactor 核心):主线程通过 epoll 监听网络 I/O 事件(如客户端连接、数据到达),同时将 evfd 的文件描述符加入 epoll 监听集合。当有新任务需要分配给子线程时,主线程向 evfd 写入一个计数器值(如 1),触发 evfd 的读事件,以此作为 “唤醒信号”。

  2. 子线程(工作线程):子线程阻塞在 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 个新任务”。

  • 避免惊群效应(可选):若多个子线程监听同一个 evfdread 操作会让所有线程被唤醒(惊群),但可通过为每个子线程分配独立 evfd 解决(主线程按需唤醒指定子线程);而 FIFO 的惊群行为较难控制。

  • 线程安全友好evfd 的 write 操作是原子的,多线程同时写入时计数器会累加,无需额外加锁;而 FIFO 的 write 可能需要同步机制避免数据错乱。

关键实现细节

  • 初始化 evfd:主线程通过 eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC) 创建 evfdEFD_NONBLOCK 避免写入阻塞,EFD_CLOEXEC 确保进程退出时自动关闭。

  • 任务队列与同步evfd 仅负责唤醒,实际任务需通过线程安全队列(如加锁的链表、pthread_mutex 保护的队列)传递,子线程被唤醒后从队列取任务。

  • 重置计数器:子线程处理完任务后,需通过 read(evfd, &cnt, 8) 读取计数器(返回值即为写入的数值),将其清零,否则 epoll 会持续触发读事件(水平触发模式下)。

总结

在 Reactor 多线程模型中,evfd 是比 FIFO 更优的唤醒选择:它更轻量、与 epoll 整合更自然、支持简单信息传递,且适合高并发场景下的线程间通知。实际开发中,libeventlibev 等 Reactor 库也常使用 evfd 作为内部事件通知机制,替代传统的管道或信号量。

 

多Reactor线程自我举荐

在 Reactor 多线程模型中,“自我举荐”(也可理解为 “主动认领” 或 “竞争调度”)是一种去中心化的任务分配思路,核心是让子线程主动竞争处理任务,而非由主线程集中分配,以此减少主线程的调度压力,提升并发效率。

核心逻辑

  1. 主线程(Reactor 核心):仅负责监听网络 I/O 事件(如数据到达),当事件发生时,将任务(如连接对象、待处理数据)放入一个 全局共享的线程安全任务队列,不参与任务分配,也不主动唤醒特定子线程。

  2. 子线程(工作线程):所有子线程处于 “主动轮询” 或 “阻塞等待” 状态,不断检查任务队列是否有新任务。一旦发现任务,就通过 原子操作(如 CAS) 或 锁竞争 主动 “认领” 任务(即 “自我举荐” 处理该任务),拿到任务后独立处理,处理完成后继续等待下一个任务。

与 “主线程唤醒” 模式的区别

  • 传统模式(如用 FIFO/evfd 唤醒):主线程分配任务后,主动通知某个子线程(或广播唤醒),子线程被动响应。
  • 自我举荐模式:主线程只 “抛任务”,子线程主动 “抢任务”,无需主线程单独唤醒,减少了主线程的调度逻辑。

关键实现细节

  • 任务队列的线程安全:必须使用支持原子操作的无锁队列(如 moodycamel::ConcurrentQueue)或带锁的队列(如 pthread_mutex 保护的链表),确保多个子线程同时抢任务时不会出现数据竞争。无锁队列更优,可避免锁竞争带来的性能损耗。

  • 子线程的等待策略

    • 若任务频繁,子线程可采用 自旋等待(循环检查队列,不放弃 CPU),减少上下文切换开销。
    • 若任务稀疏,子线程可阻塞在 条件变量(pthread_cond) 上,当队列有任务时由主线程或其他子线程唤醒(此时仍保留轻微的 “通知” 逻辑,但核心是竞争处理)。
  • 负载均衡:依赖子线程的竞争机制天然实现负载均衡 —— 空闲的子线程会更快抢到任务,避免某一子线程过载。

优势与适用场景

  • 减少主线程压力:主线程无需维护子线程状态(如负载情况),也无需设计唤醒策略,逻辑更简单,适合高并发场景。
  • 动态扩展性好:新增子线程时无需修改主线程逻辑,直接加入竞争即可,适合需要动态调整线程数的场景。
  • 适合 CPU 密集型任务:任务处理耗时较长时,子线程主动竞争可避免主线程调度延迟导致的任务堆积。

潜在问题

  • 竞争开销:若任务粒度极小(如简单数据解析),子线程抢任务的竞争(如 CAS 重试、锁争夺)可能抵消并行收益,反而降低效率。
  • 缓存抖动:多个子线程频繁访问共享队列,可能导致 CPU 缓存失效(缓存一致性开销),尤其在多核环境下。

这种模式更接近 “工作窃取”(Work Stealing)的简化版,核心是通过去中心化的竞争机制提升调度灵活性,常见于高性能并发框架(如某些 RPC 服务器、游戏服务器的任务处理模块)。

 

 

多进程 Reactor 

在 Linux 下,Reactor 模型也可扩展到多进程场景,核心思路是通过 进程间通信(IPC) 实现事件分发与任务协作,解决单进程资源限制(如 CPU 核心利用率、内存隔离),同时保持 Reactor 模式 “事件驱动、异步处理” 的特性。

多进程 Reactor 核心设计

  1. 主进程(Listener 角色)

    • 负责监听核心网络事件(如 socket 监听端口的新连接),通常通过 epoll/select 管理监听套接字(listen_fd)。
    • 不直接处理业务逻辑,而是当新连接到来时(accept 事件),通过 负载均衡策略(如轮询、最少连接数)将连接分配给子进程。
  2. 子进程(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 在高性能服务器中的经典实践。

 

 

尾声:

     历时许久,我的主线课程终于在今日正式收官。 这段学习旅程算不上一帆风顺——去年暑假的中途暂停,让原本的节奏被打乱,课程也一度搁置。好在没有彻底放弃,兜兜转转,还是咬牙把剩下的内容逐一攻克。更具挑战的是,课程要求一课对应一篇博客总结,每一次输出都需要复盘知识点、梳理思路,过程虽繁琐,却也让我对知识的掌握更扎实。 回头看,这份成果离不开机构老师们的专业指导,更要感谢始终没停下脚步的自己。那些坚持的日子,终究都变成了成长的勋章。

 

http://www.dtcms.com/a/617744.html

相关文章:

  • 【C++】C++11:智能指针
  • 把网站做成手机版创意设计师
  • 条件前缀|同余优化|栈
  • 做淘客app要网站吗大数据精准营销策略
  • 对于数据结构:链式二叉树的超详细保姆级解析—中
  • 多模态大模型对齐陷阱:对比学习与指令微调的“内耗“问题及破解方案
  • 关键词解释:F1值(F1 Score)
  • 大语言模型入门指南:从科普到实战的技术笔记(2)
  • 【RL-LLM】Self-Rewarding Language Models
  • Redis学习笔记-List列表(2)
  • 区块链与以太坊基础:环境搭建与智能合约部署
  • 二维码怎么在网站上做推广微信商店小程序制作教程
  • 毕业设计可以做哪些网站电子商务网站建设前期规划方案
  • Linux 磁盘挂载管理
  • 智能体知识库核心技术解析与实践指南——从文件处理到智能输出的全链路架构v1.2
  • 【Java 基础】 2 面向对象 - 构造器
  • dw6做网站linux做网站服务器那个软件好
  • 生成式人工智能赋能教师专业发展的机制与障碍:基于教师能动性的质性研究
  • 无锡锡山区建设局网站北京网站定制建设
  • 【Word学习笔记】Word如何转高清PDF
  • 小程序地图导航,怎样实现用户体验更好
  • 下流式接入ai
  • PDF无法打印怎么解决?
  • 南宁市网站建设哪家好企业网站模板html
  • 华为数据中心CE系列交换机级联M-LAG配置示例
  • 【HarmonyOS】性能优化——组件的封装与复用
  • 低代码平台的性能优化:解决页面卡顿、加载缓慢问题
  • 开源工程笔记:gitcode/github与性能优化
  • 微页制作网站模板手机上自己做网站吗
  • 基于51单片机的8路简易抢答器