【项目篇】从零手写高并发服务器(六):EventLoop事件循环——Reactor的心脏

文章目录

从零手写高并发服务器(六):EventLoop事件循环------Reactor的心脏

💬 开篇:EventLoop是整个Reactor模式的核心驱动。它把Poller监控到的事件分发给对应的Channel处理,同时还要处理线程间的任务调度。理解了EventLoop,你就理解了整个服务器的运转逻辑。

👍 点赞、收藏与分享:这是整个项目最关键的一篇,建议反复阅读理解。

🚀 循序渐进:EventLoop设计思路 → 线程安全问题 → 完整实现 → 补全Channel → 测试验证。


一、EventLoop的设计思路

1.1 EventLoop是什么?

EventLoop就是一个事件循环,它的核心逻辑非常简单:

bash 复制代码
while (true) {
    1. 调用Poller等待事件就绪
    2. 遍历活跃的Channel,调用HandleEvent处理事件
    3. 执行任务队列中的任务
}

1.2 为什么需要任务队列?

在多线程Reactor模式中,有一个关键原则:一个连接的所有操作都必须在同一个EventLoop线程中执行

但是,其他线程可能需要对某个连接进行操作(比如主线程要把新连接分配给子线程)。这时候不能直接操作,需要把任务放到目标EventLoop的任务队列中,让目标线程自己来执行。

bash 复制代码
线程安全问题:

  主线程(EventLoop_0)                 子线程(EventLoop_1)
       │                                    │
       │ 新连接到来                           │ 正在处理事件
       │     ↓                               │
       │ 需要把连接交给子线程                   │
       │     ↓                               │
       │ 不能直接操作!                        │
       │ 而是把任务放到子线程的任务队列中          │
       │     ↓                               │
       │ eventfd通知子线程                     │ ← 被唤醒
       │                                    │ 执行任务队列中的任务

1.3 eventfd的作用

Poller在等待事件时是阻塞的(或者超时等待),如果其他线程往任务队列里放了任务,怎么让EventLoop立刻醒过来执行?

答案是用 eventfd

  • 创建一个eventfd,加入到Poller监控
  • 其他线程往任务队列放任务后,向eventfd写入数据
  • Poller检测到eventfd可读,EventLoop被唤醒
  • 唤醒后执行任务队列中的任务
cpp 复制代码
#include <sys/eventfd.h>
// 创建eventfd
int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
// 写入唤醒
uint64_t val = 1;
write(efd, &val, sizeof(val));
// 读取(消费唤醒信号)
uint64_t val;
read(efd, &val, sizeof(val));

二、EventLoop完整实现

2.1 需要的头文件

server.hpp 中添加:

cpp 复制代码
#include <sys/eventfd.h>
#include <thread>
#include <mutex>

2.2 EventLoop类实现

在Poller类后面添加:

cpp 复制代码
// ==================== EventLoop事件循环模块 ====================
using Functor = std::function<void()>;

class EventLoop {
private:
    using Functor = std::function<void()>;
    std::thread::id _thread_id;  // 线程ID
    int _event_fd;               // eventfd唤醒用
    std::unique_ptr<Channel> _event_channel;  // eventfd的事件管理
    Poller _poller;              // Poller对象
    std::vector<Functor> _tasks; // 任务池
    std::mutex _mutex;           // 实现任务池操作的线程安全
    
    // 用于定时器管理的TimerWheel,后面会加上
    // std::unique_ptr<TimerWheel> _timer_wheel;
    
public:
    EventLoop()
        : _thread_id(std::this_thread::get_id()),
          _event_fd(CreateEventFd()),
          _event_channel(new Channel(this, _event_fd))
    {
        // 给eventfd设置可读事件回调,读取eventfd中的数据(消费唤醒信号)
        _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
        // 启动eventfd的读事件监控
        _event_channel->EnableRead();
    }
    
    // 创建eventfd
    static int CreateEventFd() {
        int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
        if (efd < 0) {
            ERR_LOG("CREATE EVENTFD FAILED!!");
            abort();
        }
        return efd;
    }
    
    // 读取eventfd
    void ReadEventfd() {
        uint64_t res = 0;
        int ret = read(_event_fd, &res, sizeof(res));
        if (ret < 0) {
            // EINTR -- 被信号打断;EAGAIN -- 表示无数据可读
            if (errno == EINTR || errno == EAGAIN) {
                return;
            }
            ERR_LOG("READ EVENTFD FAILED!");
            abort();
        }
        return;
    }
    
    // 写入eventfd用于唤醒阻塞的epoll
    void WeakUpEventFd() {
        uint64_t val = 1;
        int ret = write(_event_fd, &val, sizeof(val));
        if (ret < 0) {
            if (errno == EINTR) {
                return;
            }
            ERR_LOG("WRITE EVENTFD FAILED!");
            abort();
        }
        return;
    }
    
    // 判断将要执行的任务是否处于当前线程中,如果是则不需要压入队列
    bool IsInLoop() {
        return (_thread_id == std::this_thread::get_id());
    }
    
    // 判断并执行任务
    void RunInLoop(const Functor &cb) {
        if (IsInLoop()) {
            return cb();
        }
        return QueueInLoop(cb);
    }
    
    // 将操作压入任务池
    void QueueInLoop(const Functor &cb) {
        {
            std::unique_lock<std::mutex> _lock(_mutex);
            _tasks.push_back(cb);
        }
        // 唤醒有ัพ能阻塞的epoll
        WeakUpEventFd();
    }
    
    // 执行任务池中的所有任务
    void RunAllTask() {
        std::vector<Functor> functor;
        {
            std::unique_lock<std::mutex> _lock(_mutex);
            _tasks.swap(functor);
        }
        for (auto &f : functor) {
            f();
        }
        return;
    }
    
    // 添加/修改描述符的事件监控
    void UpdateEvent(Channel *channel) { return _poller.UpdateEvent(channel); }
    // 移除描述符的监控
    void RemoveEvent(Channel *channel) { return _poller.RemoveEvent(channel); }
    
    // 三步走:事件监控 → 事件处理 → 执行任务
    void Start() {
        while (1) {
            // 1. 事件监控
            std::vector<Channel *> actives;
            _poller.Poll(&actives);
            // 2. 事件处理
            for (auto &channel : actives) {
                channel->HandleEvent();
            }
            // 3. 执行任务池中的任务
            RunAllTask();
        }
    }
};

三、补全Channel的Update和Remove

现在EventLoop实现好了,我们可以补全Channel中之前留的坑了。

在Channel类的外部(类定义之后,但在EventLoop类定义之后)添加:

cpp 复制代码
// Channel的Update和Remove实现(需要在EventLoop定义之后)
void Channel::Update() { _loop->UpdateEvent(this); }
void Channel::Remove() { _loop->RemoveEvent(this); }

注意:因为Channel中使用了EventLoop的方法,而EventLoop中又使用了Channel,存在循环依赖。解决方法是:

  • Channel类中只做前向声明 class EventLoop;
  • Channel的 Update()Remove() 方法放到EventLoop类定义之后实现

四、测试EventLoop

bash 复制代码
cd ~/TcpServer/test
vim eventloop_test.cpp
cpp 复制代码
#include "../source/server.hpp"

int main() {
    EventLoop loop;
    
    // 创建监听套接字
    Socket srv_sock;
    srv_sock.CreateServer(8500);
    srv_sock.NonBlock(); // 设置非阻塞
    DBG_LOG("服务器启动成功,监听端口8500,fd=%d", srv_sock.Fd());
    
    // 创建Channel
    Channel channel(&loop, srv_sock.Fd());
    
    // 设置可读事件回调
    channel.SetReadCallback([&](){
        int newfd = accept(srv_sock.Fd(), NULL, NULL);
        if (newfd >= 0) {
            DBG_LOG("获取新连接,fd=%d", newfd);
            close(newfd);
        }
    });
    
    // 启动读事件监控
    channel.EnableRead();
    
    DBG_LOG("开始事件循环...");
    loop.Start(); // 这里会阻塞,进入事件循环
    
    return 0;
}

编译运行:

bash 复制代码
g++ -std=c++11 eventloop_test.cpp -o eventloop_test -lpthread
./eventloop_test

另开终端测试:

bash 复制代码
./tcp_cli

服务端输出

bash 复制代码
wsh@VM-16-2-ubuntu:~/TcpServer/test$ ./eventloop_test 
[0x7f8a1b2c3740 16:30:05 ../source/server.hpp:156] SIGPIPE INIT
[0x7f8a1b2c3740 16:30:05 eventloop_test.cpp:10] 服务器启动成功,监听端口8500,fd=3
[0x7f8a1b2c3740 16:30:05 eventloop_test.cpp:23] 开始事件循环...
[0x7f8a1b2c3740 16:30:12 eventloop_test.cpp:16] 获取新连接,fd=6

EventLoop事件循环工作正常!Channel → Poller → EventLoop 三者串联成功。


五、提交代码

bash 复制代码
cd ~/TcpServer
git add .
git commit -m "实现EventLoop事件循环模块,补全Channel的Update/Remove"
git push

六、本篇总结

模块 功能
EventLoop 事件循环,驱动整个Reactor
eventfd 线程间唤醒机制
任务队列 线程安全的跨线程任务调度

EventLoop的三步走核心逻辑:

bash 复制代码
while (1) {
    Poller.Poll()     → 等待事件就绪
    HandleEvent()     → 处理就绪事件
    RunAllTask()      → 执行任务队列
}

当前 server.hpp 结构:

cpp 复制代码
// 日志宏
// Buffer类
// NetWork类(SIGPIPE处理)
// Socket类
// Channel类
// Poller类
// EventLoop类
// Channel::Update() / Channel::Remove() 实现

💬 下一篇预告:实现TimerWheel定时器和EventLoopThread/EventLoopThreadPool线程池,让服务器支持多线程!


相关推荐
Lary_c2 小时前
【测试自动化】pytest + Allure 完整学习指南
运维·自动化·pytest
bai_lan_ya2 小时前
linux -- 文件IO
linux·服务器
学嵌入式的小杨同学2 小时前
STM32 进阶封神之路(十八):RTC 实战全攻略 —— 时间设置 + 秒中断 + 串口更新 + 闹钟功能(库函数 + 代码落地)
c++·stm32·单片机·嵌入式硬件·mcu·架构·硬件架构
学嵌入式的小杨同学2 小时前
STM32 进阶封神之路(十七):RTC 实时时钟深度解析 —— 从时钟源到寄存器配置(底层原理 + 面试重点)
c++·stm32·单片机·嵌入式硬件·mcu·硬件架构·pcb
.select.2 小时前
STL下常见容器底层数据结构
开发语言·c++
老鱼说AI2 小时前
CUDA架构与高性能程序设计:多维网格与数据
c++·人工智能·深度学习·神经网络·机器学习·语言模型·cuda
海边的梦2 小时前
救命!此电脑网络位置异常?AD域排错3步封神,DNS/NetLogon/GPO根因一键定位
服务器·开发语言·php
林鸿群2 小时前
Ubuntu 26.04 本地安装 GitLab CE 完整教程(非 Docker 方式)
linux·ubuntu·gitlab·私有部署·代码托管·ubuntu 26.04·omnibus
Azure DevOps2 小时前
Azure DevOps:应用远程MCP服务器,提升工作效率
服务器·microsoft·flask·azure·devops