文章目录
-
- 从零手写高并发服务器(六):EventLoop事件循环------Reactor的心脏
- 一、EventLoop的设计思路
-
- [1.1 EventLoop是什么?](#1.1 EventLoop是什么?)
- [1.2 为什么需要任务队列?](#1.2 为什么需要任务队列?)
- [1.3 eventfd的作用](#1.3 eventfd的作用)
- 二、EventLoop完整实现
-
- [2.1 需要的头文件](#2.1 需要的头文件)
- [2.2 EventLoop类实现](#2.2 EventLoop类实现)
- 三、补全Channel的Update和Remove
- 四、测试EventLoop
- 五、提交代码
- 六、本篇总结
从零手写高并发服务器(六):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线程池,让服务器支持多线程!