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

muduo网络库(八):EventLoop 事件循环

muduo网络库(八):EventLoop 事件循环

  • muduo网络库(八):EventLoop 事件循环
    • 概述
    • EventLoop 的内部结构
    • loop 主循环
    • 跨线程唤醒机制
      • 为什么需要 wakeup
      • 跨线程投递流程
    • doPendingFunctors 的巧妙设计
    • One Loop Per Thread 多线程模型
      • mainLoop 与 subLoops 的分工
      • 多线程协作时间线
      • wakeup 如何融入流程
    • 单个 EventLoop 的生命周期

muduo网络库(八):EventLoop 事件循环

概述

EventLoop相当于 muduo 的主反应堆,其中包含了Channel类和Poller类。muduo 采用经典的One Loop Per Thread模型——每个线程拥有一个 EventLoop,负责该线程内所有文件描述符的事件监听和回调分发。

EventLoop 的核心职责可以概括为两点:

  • IO 事件分发:通过 Poller 监听 fd,事件就绪后调用 Channel 的回调
    • 跨线程任务执行:通过 wakeupFd 唤醒机制,让其他线程安全地投递任务到本线程执行

EventLoop 的内部结构

EventLoop ├── poller_ (EpollPoller) │ ├── epollFd_ ← epoll 实例 │ └── channels_ ← 所有注册的 fd │ ├── listenfd → Channel (主线程) │ ├── connfd1 → Channel (工作线程) │ ├── connfd2 → Channel (工作线程) │ └── ... ├── wakeupFd_ ← 跨线程唤醒用 └── wakeupChannel_ ← 封装 wakeupFd_

除了 Poller 管理的 IO 通道外,EventLoop 还有一个特殊的wakeupFd_——这是eventfd创建的文件描述符,专门用于跨线程唤醒。当其他线程需要让当前 EventLoop 立即处理某个任务时,就往这个 fd 写入数据,让epoll_wait立刻返回。

loop 主循环

loop()是 EventLoop 的核心函数,也是整个事件驱动引擎的心脏。它的执行流程如下:

┌──────────────────────────────────────────────────────────────────┐ │ EventLoop::loop() 主循环 │ ├──────────────────────────────────────────────────────────────────┤ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 1. EpollPoller::poll() → epoll_wait() 阻塞等待事件 │ │ │ └───────────────────────────┬────────────────────────────────┘ │ │ ↓ 有事件发生(或被wakeup唤醒) │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 2. fillActiveChannels() 收集活跃的 Channel │ │ │ └───────────────────────────┬────────────────────────────────┘ │ │ ↓ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 3. 遍历 activeChannels → Channel::handleEvent() │ │ │ │ ↓ │ │ │ │ 调用各回调:readCallback_/writeCallback_/closeCallback_...│ │ │ └────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 4. doPendingFunctors() 执行其他线程投递的任务 │ │ │ └────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘

其中,channel->handleEvent()负责处理 epoll 返回的 IO 事件,而doPendingFunctors()负责处理其他线程投递的任务。

关键细节:EventLoop 在构造函数中已经将 wakeupFd 的读回调绑定到handleRead函数,所以当handleEvent()处理 wakeup 事件时,实际上只是消费唤醒信号(读取 8 字节数据),真正的任务在pendingFunctors_队列中等待执行。

跨线程唤醒机制

为什么需要 wakeup

假设线程 B 的 EventLoop 正在epoll_wait()中阻塞等待事件,此时线程 A 想让它执行一个任务(比如注册新的 Channel)。如果不做特殊处理,线程 B 根本不知道有任务到来,只能等下一个 IO 事件到来才会醒来——这可能要等很久。

wakeupFd_就是为了解决这个问题:线程 A 往wakeupFd_写入数据,epoll 立刻检测到可读事件,线程 B 从epoll_wait()返回,然后执行doPendingFunctors()处理任务。

跨线程投递流程

线程A 线程B (EventLoop线程) ┌─────────────────────────┐ ┌─────────────────┐ │ 线程池中获取subLoop │ │ │ │ runInLoop(func) │ │ poll() 阻塞等待 │ │ └─ isInLoopThread() → false │ │ │ └─ queueInLoop(func) │ │ │ ├─ func → queue │ │ │ └─ wakeup() │ │ │ │ │ │ │ ▼ │ │ │ write(wakeupFd) ──────────▶ poll()返回│ │ │ │ │ │ │ │ handleEvent(wakeupChannel)│ │ │ │ └─ read(wakeupFd) 消费信号│ │ │ │ │ │ │ │ doPendingFunctors() │ │ │ │ └─ 执行 task │ │ │ │ │ └─────────────────────────┘ └─────────────────┘

当线程 A 向线程 B 投递任务时:

  1. 先将任务放入pendingFunctors_队列(加锁保护)
    1. 再调用wakeup()wakeupFd_写入数据
    1. 线程 B 的epoll_wait()检测到 wakeupFd 可读,立即返回
    1. 执行doPendingFunctors()依次处理队列中的任务
      wakeup 的作用只是让 epoll_wait 立即返回,任务早就投入队列了。就算不立即唤醒,下次有 IO 事件时也会调用doPendingFunctors()处理这些任务。但为了实时性,通常需要立即唤醒。

这种设计还有一个优势:当批量投递多个任务时,只需一次唤醒,doPendingFunctors()会一次性处理所有积攒的任务。

doPendingFunctors 的巧妙设计

doPendingFunctors()不是直接遍历pendingFunctors_执行,而是先将其交换到一个局部变量中:

voiddoPendingFunctors(){std::vector<Functor>functors;{MutexLockGuardlock(mutex_);functors.swap(pendingFunctors_);// 交换而非拷贝}for(constFunctor&fn:functors){fn();// 在锁外执行}}

这个swap设计非常精妙:

  • 缩小临界区:只在 swap 时加锁,执行任务时不需要持锁,减少锁竞争
    • 避免死锁:任务内部可能再次调用runInLoop(),如果执行时仍持锁就会死锁
    • 提高吞吐:新投递的任务在执行期间可以继续加入队列,下一轮循环再处理

One Loop Per Thread 多线程模型

mainLoop 与 subLoops 的分工

muduo 的多线程模型遵循One Loop Per Thread原则:

  • mainLoop:主线程的 EventLoop,负责accept 新连接
  • subLoops:N 个子线程的 EventLoop,负责处理已连接的 IO 读写
    它们之间不靠消息队列、不靠管道、不靠复杂通信,只靠wakeup()+ 锁 +pendingFunctors_完成全部交互。

多线程协作时间线

时间线 → 主线程 mainLoop: poll() → 检测到 listenfd 可读 → accept() → 获取 connfd ↓ 将 connfd 封装为 Channel,通过 queueInLoop() 发送到工作线程 ↓ 继续 poll() 等待下一个事件 工作线程 subLoop1: poll() 阻塞中 ←── 被 wakeupFd 唤醒 ↓ 执行 pendingFunctors 中的任务(注册新 Channel) ↓ poll() 继续监听 connfd 事件 ↓ 检测到 connfd 可读 → 调用 Channel 的 readCallback_

wakeup 如何融入流程

  1. mainLoop 拿到新连接:主线程accept到新的客户端连接
    1. mainLoop 把任务发给 subLoop
subLoop->runInLoop(把新连接交给你处理);
  1. runInLoop 内部做两件事:加锁 → 将任务放入pendingFunctors_wakeup()唤醒 subLoop
    1. subLoop 被唤醒后:从doPendingFunctors()中取出函数,swap 后依次执行
      wakeup 的本质:往 subLoop 自己的 eventfd 写一个 8 字节数据,让 subLoop 从epoll_wait()阻塞中立刻醒来。

单个 EventLoop 的生命周期

从 Channel 注册到事件处理的完整流程:

用户注册事件 ↓ Channel::enableXxx() → Channel::update() ↓ EventLoop::updateChannel() ← 新 channel 加入 channels_ Map ↓ EpollPoller::update() ↓ 根据 channel 的 index 判断是添加还是删除 epoll_ctl(ADD/MOD/DEL) ← 内核注册 ``` 注册完成后,EventLoop 的 `loop()` 循环就会持续监听这些 fd,一旦事件就绪就触发回调。 ## 设计精髓总结 | 设计点 | 做法 | 收益 | |--------|------|------| | One Loop Per Thread | 每线程一个 EventLoop | 线程内无需加锁,避免竞争 | | wakeupFd 唤醒 | eventfd + epoll 监听 | 跨线程通信低延迟,立即可达 | | pendingFunctors | 任务队列 + swap | 临界区最小化,避免死锁 | | 事件驱动 | poll → handleEvent → doPendingFunctors | IO 与任务统一在事件循环中处理 | | mainLoop/subLoops | 主线程 accept,子线程处理 IO | 新连接分发均衡,扩展性强 | EventLoop 是 muduo 网络库的灵魂,它将 Channel(事件通道)、Poller(IO 复用)、wakeupFd(跨线程唤醒)和 pendingFunctors(任务队列)有机整合,构建了一个高效、可扩展、线程安全的事件驱动引擎。
http://www.jsqmd.com/news/1314599/

相关文章:

  • UE5集成GDAL动态库实战:打通地理数据与虚幻引擎的桥梁
  • Wio Terminal SPI通信全解析:从原理到实战驱动屏幕与SD卡
  • Excel数据对比全攻略:从VLOOKUP到Power Query,高效核对两列数据
  • Meshroom:零代码实现专业级3D重建,让照片“活“起来
  • 应对气象大数据处理的挑战:MeteoInfo平台架构设计与工程实践深度解析
  • Go语言反射(Reflection)详解:原理、作用、TypeOf与ValueOf区别及常用函数
  • Windows 11终极优化指南:如何通过原生工具和脚本实现系统性能翻倍
  • 单片机毕业设计-基于 STM32F103 的多传感器智能调光台灯开发 基于多传感融合的人体感应护眼照明装置设计(018401)
  • 2026 年 8 月北京西城区钻戒变现实测,持证实体门店仪器专业验钻 - 生活时报
  • 2026 江苏 PCBA 打样转量产指南|研发打样、小批量试产、大批量量产衔接与优质厂商选型
  • C语言头文件与源文件关系详解:从编译原理到工程实践
  • 开源AI Markdown桌面应用:从部署到深度集成的全流程实践指南
  • 2026年8月鹅卵石滤料销售厂家推荐,园林鹅卵石/景观砾石/露营砾石/五彩鹅卵石/石英砂,鹅卵石滤料供应厂家推荐 - 品牌推荐师
  • Quorum 选型 AMD 方案入围后:我连夜补的 4 项技术债清单
  • MKS Monster8 8轴主板终极指南:从零开始配置你的3D打印机控制板
  • 3个痛点,一个方案:Slurm-web如何重塑HPC集群管理体验
  • 2026苏州托福培训机构全面测评|优选苏州爱特精英教育 - 产品评测官
  • ReachyMini桌面机器人开发指南:从SDK控制到多模态交互实战
  • PVZ创意工坊最高难度通关策略:从阵容搭配到微操技巧
  • 04数据结构
  • 2026美国本科申请规划,北京留学中介推荐参考 - 2027品牌AI展
  • 基于Xadow与QMC5883L的电子罗盘:模块化硬件的高精度航向感知实践
  • Arduino驱动LED点阵屏:Rainbowduino v3.0硬件解析与项目实战
  • ClickHouse-Java客户端连接诊断实战:5大异常场景深度解析与高效解决方案
  • MPR121电容触摸传感器:从原理到实战,打造稳定交互体验
  • 快慢指针与链表反转:O(1)空间复杂度判断回文链表详解
  • 东莞医院搬家
  • 商业综合体舞台LED显示系统设计分析
  • OBS实时字幕插件完全指南:3步实现专业直播字幕
  • 2026淮南中考100分左右还能读什么学校? 合肥这所公办院校8月补录中 - 小张zc