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 模式最经典的"三步走":
- 事件监控 :调用
Poller::Poll,阻塞等待事件,返回活跃的Channel*列表。 - 事件处理 :遍历每个活跃
Channel,调用其HandleEvent(),执行相应的回调。 - 执行任务 :执行任务队列中积累的所有函数(可能来自其他线程的
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); }