仿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_fd 的 Channel,当 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_CLOEXEC 和 EFD_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); }
相关推荐
j7~1 分钟前
【Linux网络加餐】《HTTP 会话保持:Cookie 与 Session 原理及实验验证》---详解
linux·网络·网络协议·http·cookie·session
我是小灰灰吖7 分钟前
Ubuntu 22.04 为程序添加桌面启动图标(.desktop 完整实践)
linux·ubuntu
Linux运维技术栈7 分钟前
CVE-2026-91403 高危漏洞预警:Java JSSE 信任链被攻破 全场景修复方案与Linux系统级彻底清理实操
java·linux·安全·jdk·信任链
SHENGWEI胜为9 分钟前
服务器 IP 地址冲突如何排查?从 DHCP 租约到 ARP 缓存定位故障
服务器·tcp/ip·缓存
H.莓飛11 分钟前
【数据结构】快速排序专题
linux·服务器·c语言·数据结构·visual studio
箓维12 分钟前
网络基础(2)IP和MAC的区别与报文封装
linux·网络
小HANN12 分钟前
(索引 + SQL 优化):《MySQL 底层探秘②:B + 树索引与 Explain 实战,根治线上慢 SQL》
linux·运维·服务器·经验分享
lisanmengmeng14 分钟前
pushgateway
linux·运维·服务器·网络
努力实干16 分钟前
Qt/C++ 模块化设计与工程组织
开发语言·c++·qt
zbtlink18 分钟前
从 DDR4 颗粒到路由器价签:一轮存储涨价的三层传导与复算
网络·网络安全·智能路由器