仿muduo库实现高并发服务器—EventLoop

EventLoop 封装详解

一、整体结构

cpp 复制代码
class EventLoop {
private:
    using Functor = std::function<void()>;
    std::thread::id _thread_id;
    int _event_fd;
    std::unique_ptr<Channel> _event_channel;
    Poller _poller;
    std::vector<Functor> _tasks;
    std::mutex _mutex;
    TimerWheel _timer_wheel;

public:
    // 任务处理
    void RunAllTask();
    static int CreateEventFd();
    void ReadEventfd();
    void WeakUpEventFd();

    // 构造函数
    EventLoop();

    // 循环
    void Start();

    // 线程判断
    bool IsInLoop();
    void AssertInLoop();
    void RunInLoop(const Functor &cb);
    void QueueInLoop(const Functor &cb);

    // 转发接口
    void UpdateEvent(Channel *channel);
    void RemoveEvent(Channel *channel);
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb);
    void TimerRefresh(uint64_t id);
    void TimerCancel(uint64_t id);
    bool HasTimer(uint64_t id);
};

EventLoop 的作用:

  • 拥有一个 Poller,负责监控所有 Channel 的事件。
  • 拥有一个 TimerWheel,负责定时任务。
  • 拥有一个 eventfd 和对应的 Channel,用于唤醒自己。
  • 拥有一个任务队列,允许其他线程投递任务,保证线程安全。
  • 循环执行:等待事件 → 处理事件 → 执行任务队列

二、成员变量详解

cpp 复制代码
using Functor = std::function<void()>;
std::thread::id _thread_id;
int _event_fd;
std::unique_ptr<Channel> _event_channel;
Poller _poller;
std::vector<Functor> _tasks;
std::mutex _mutex;
TimerWheel _timer_wheel;
  • Functor:任务类型,无参数无返回值,和之前的 TaskFunc 本质一样。
  • _thread_id:保存创建该 EventLoop 时的线程 ID,用于后续判断当前线程是否为循环所在线程。
  • _event_fd:eventfd 描述符,用来唤醒可能阻塞在 epoll_wait 的循环。
  • _event_channel:包装 _event_fdChannel,当 eventfd 可读时调用回调。
  • _poller:事件监控器,内部封装了 epoll。
  • _tasks:任务队列,保存其他线程投递的函数。
  • _mutex:保护 _tasks 的互斥锁,保证多线程安全。
  • _timer_wheel:定时器模块,和 EventLoop 绑定。

三、任务处理相关私有函数

1. RunAllTask

cpp 复制代码
void RunAllTask() {
    std::vector<Functor> functor;
    {
        std::unique_lock<std::mutex> _lock(_mutex);
        _tasks.swap(functor);
    }
    for (auto &f : functor) {
        f();
    }
    return;
}
  • 先从任务队列中"交换"出所有任务,避免长时间持有锁。
  • 然后逐个执行任务。
  • 使用 swap 而不是逐项取出,可以减少锁竞争,提高效率。

注意 :任务执行时没有锁,所以任务内部可以安全地调用 QueueInLoop 或其他接口,不会死锁。

2. CreateEventFd

cpp 复制代码
static int CreateEventFd() {
    int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
    if (efd < 0) {
        ERR_LOG("CREATE EVENTFD FAILED!!");
        abort();
    }
    return efd;
}

创建 eventfd,初始值为 0,设置 EFD_CLOEXECEFD_NONBLOCK

我们之前已经讲过 eventfd 的作用:用于跨线程唤醒。

3. ReadEventfd

cpp 复制代码
void ReadEventfd() {
    uint64_t res = 0;
    int ret = read(_event_fd, &res, sizeof(res));
    if (ret < 0) {
        if (errno == EINTR || errno == EAGAIN) {
            return;
        }
        ERR_LOG("READ EVENTFD FAILED!");
        abort();
    }
    return;
}

当 eventfd 可读时被调用,读取其中的计数,将其清零。

如果 EINTR(信号打断)或 EAGAIN(非阻塞下无数据),直接返回,不算错误。

4. WeakUpEventFd(拼写错误,应为 WakeUpEventFd)

cpp 复制代码
void WeakUpEventFd() {
    uint64_t val = 1;
    int ret = write(_event_fd, &val, sizeof(val));
    if (ret < 0) {
        if (errno == EINTR) {
            return;
        }
        ERR_LOG("READ EVENTFD FAILED!");
        abort();
    }
    return;
}

向 eventfd 写入一个 8 字节值 1,使其变为可读,从而唤醒阻塞在 epoll_wait 的循环。

这里的日志信息写成了 "READ EVENTFD FAILED!",实际上是写错误,不影响功能。


四、构造函数

cpp 复制代码
EventLoop()
    : _thread_id(std::this_thread::get_id()),
      _event_fd(CreateEventFd()),
      _event_channel(new Channel(this, _event_fd)),
      _timer_wheel(this)
{
    _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
    _event_channel->EnableRead();
}
  • 记录当前线程 ID,作为事件循环所属线程。
  • 创建 eventfd,用于唤醒。
  • 创建对应的 Channel,传入 this 作为所属 EventLoop。
  • 初始化 _timer_wheel,也传入 this
  • 设置 eventfd 的读回调为 ReadEventfd,并启用读事件监控。

这样,一旦其他线程向 eventfd 写入数据,epoll_wait 就会返回这个 fd 的可读事件,然后调用 ReadEventfd 读取并继续处理任务。


五、核心循环:Start

cpp 复制代码
void Start() {
    while(1) {
        // 1. 事件监控
        std::vector<Channel *> actives;
        _poller.Poll(&actives);

        // 2. 事件处理
        for (auto &channel : actives) {
            channel->HandleEvent();
        }

        // 3. 执行任务
        RunAllTask();
    }
}

这是 Reactor 模式最经典的"三步走":

  1. 事件监控 :调用 Poller::Poll,阻塞等待事件,返回活跃的 Channel* 列表。
  2. 事件处理 :遍历每个活跃 Channel,调用其 HandleEvent(),执行相应的回调。
  3. 执行任务 :执行任务队列中积累的所有函数(可能来自其他线程的 QueueInLoop)。

六、线程安全与任务投递

1. IsInLoop 和 AssertInLoop

cpp 复制代码
bool IsInLoop() {
    return (_thread_id == std::this_thread::get_id());
}

void AssertInLoop() {
    assert(_thread_id == std::this_thread::get_id());
}
  • IsInLoop:判断当前线程是否是事件循环所在线程。
  • AssertInLoop:在 debug 模式下断言必须在循环线程中调用,用于检查那些不允许跨线程调用的函数。

2. RunInLoop

cpp 复制代码
void RunInLoop(const Functor &cb) {
    if (IsInLoop()) {
        return cb();
    }
    return QueueInLoop(cb);
}
  • 如果当前线程就是事件循环线程,则立即执行 cb
  • 否则,将 cb 投递到任务队列,并唤醒事件循环。
  • 这个接口是 muduo 中非常常用的:允许任意线程安全地执行某个操作,而操作会在正确线程中运行。

3. QueueInLoop

cpp 复制代码
void QueueInLoop(const Functor &cb) {
    {
        std::unique_lock<std::mutex> _lock(_mutex);
        _tasks.push_back(cb);
    }
    // 唤醒有可能因为没有事件就绪而导致的 epoll 阻塞
    WeakUpEventFd();
}
  • 加锁将任务放入队列,然后解锁。
  • 调用 WeakUpEventFd,唤醒阻塞在 epoll_wait 的循环,让它能够及时处理这个任务。

为什么要唤醒?

如果事件循环阻塞在 epoll_wait 上,而当前没有任何其他事件发生,那么新加入的任务就不会被执行。

通过 eventfd 写入一个值,让 epoll_wait 立即返回,从而循环能够继续执行 RunAllTask


七、接口转发

cpp 复制代码
void UpdateEvent(Channel *channel) { return _poller.UpdateEvent(channel); }
void RemoveEvent(Channel *channel) { return _poller.RemoveEvent(channel); }
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb) { return _timer_wheel.TimerAdd(id, delay, cb); }
void TimerRefresh(uint64_t id) { return _timer_wheel.TimerRefresh(id); }
void TimerCancel(uint64_t id) { return _timer_wheel.TimerCancel(id); }
bool HasTimer(uint64_t id) { return _timer_wheel.HasTimer(id); }
相关推荐
无忧.芙桃5 小时前
C++内存管理
c语言·开发语言·c++·青少年编程
是程不是序5 小时前
信创终端安全管理怎么落地?纳管、控权、护数据三段走
运维·网络·安全·电脑·系统安全
服务器硬件项目经理5 小时前
第14篇_服务器主板上MOSFET的三大应用
运维·服务器·人工智能·单片机·硬件工程·硬件
AI_Auto5 小时前
架构视角看数字化转型|完整复盘:架构先行,完成数字化组织与运营重构
网络·重构·架构
郝学胜_神的一滴5 小时前
C++11 工程级应用 11:编译期黑魔法,告别重复烂代码
c++·visual studio
疯狂打码的少年6 小时前
【计算机网络】TCP协议(三次握手、可靠传输、流量控制)
网络·笔记·tcp/ip·计算机网络
朽棘不雕6 小时前
动态库的理解
linux
AIGC大时代6 小时前
OpenAI Agents SDK 工程笔记:tool 调用循环、max_turns 与生产禁区
服务器·数据库·笔记·tool·max_turns·functiontool·生产禁区
zyeyeye6 小时前
C++入门:从HelloWorld到命名空间揭秘
c语言·开发语言·c++·算法
励志不掉头发的内向程序员6 小时前
【LibreCAD 2D架构】从 addHistory 到 RS_UndoCycle:LibreCAD 里的两套 History 与撤销机制
开发语言·c++·qt·学习·系统架构