本文聚焦C++ Reactor模式网络库的核心设计,提炼回调机制、组件关系、多线程架构 三大核心要点,去除冗余描述,保留关键逻辑和设计细节,核心围绕事件驱动、I/O监听与处理分离、One Loop Per Thread展开。
一、Reactor模式概述
Reactor是事件驱动设计模式 ,核心是将I/O事件的监听与处理分离:由Reactor监听文件描述符(fd)的I/O事件,触发后分发到对应处理器(Handler)处理,基于epoll实现I/O多路复用,是高性能网络库的基础。
核心组件及职责
各组件职责单一、解耦,构成Reactor的核心骨架,核心依赖关系:Channel ↔ Poller ↔ EventLoop,上层组件(Acceptor/Connection等)基于基础组件封装业务逻辑。
| 组件 | 核心职责 |
|---|---|
| Channel | 通用fd封装,绑定fd、关注的事件(EPOLLIN/EPOLLOUT等)和对应回调函数 |
| Poller | 封装epoll,负责fd的事件注册/修改/删除,以及epoll_wait监听事件 |
| EventLoop | 事件循环主体,驱动Poller监听事件、分发事件到Channel处理,管理定时器/任务队列 |
| Acceptor | 封装listen_fd,监听端口、接受客户端新连接 |
| Connection | 封装conn_fd,管理已建立的TCP连接,处理读写/关闭/错误事件 |
| TimerWheel | 时间轮定时器,基于timerfd实现,管理超时事件(如连接超时、心跳超时) |
| TcpServer | 对外统一接口,整合EventLoop/ThreadPool/Acceptor等所有组件,向用户提供API |
| LoopThreadPool | 多线程架构核心,管理多个LoopThread,实现One Loop Per Thread |
组件核心关系
EventLoop是每个线程的核心,驱动Poller完成事件监听;Channel是fd和事件的桥梁,所有需要监听fd的组件都会创建Channel;Poller是EventLoop的内部实现,对外隐藏epoll细节。
二、回调机制详解
回调是Reactor模式的核心驱动 ,关键要理清回调的设置方、持有方、触发时机 ,整体为自上而下传递、自下而上触发的层级结构,Channel是最底层的回调载体。
1. Channel的核心回调槽
Channel持有4个基础I/O事件的回调,是所有事件触发的入口,由HandleEvent()根据epoll返回的事件(_revents)触发对应回调:
C++
class Channel {
private:
Callback _read_callback; // 可读(EPOLLIN)
Callback _write_callback; // 可写(EPOLLOUT)
Callback _close_callback; // 对端关闭(EPOLLHUP)
Callback _error_callback; // 错误(EPOLLERR)
public:
void HandleEvent() {
if (_revents & EPOLLIN) _read_callback();
if (_revents & EPOLLOUT) _write_callback();
if (_revents & EPOLLHUP) _close_callback();
if (_revents & EPOLLERR) _error_callback();
}
};
2. 关键回调链(核心触发流程)
回调分为底层I/O回调 (Channel级别,绑定组件成员函数)和上层业务回调(用户级别,由用户自定义),核心回调链有3条:
(1)eventfd跨线程唤醒回调链(EventLoop核心)
用于其他线程向EventLoop线程投递任务,避免跨线程直接操作epoll,保证线程安全:
其他线程QueueInLoop(task) → 写入eventfd使其可读 → EventLoop的eventfd Channel触发读回调 → 执行投递的所有task。
(2)Acceptor新连接回调链
客户端connect() → listen_fd可读 → Acceptor的Channel触发读回调 → Acceptor::OnAccept()执行accept → 触发TcpServer的_new_conn_cb → TcpServer创建Connection并设置业务回调。
(3)Connection读事件回调链(业务核心,两层回调)
Connection是唯一拥有两层回调的组件,底层回调绑定自身成员函数,上层回调由用户自定义,是用户处理业务的入口:
客户端发数据 → conn_fd可读 → Connection的Channel触发读回调(第一层:I/O回调 Connection::OnRead())→ 读取数据到缓冲区 → 触发第二层:业务回调_message_cb → 执行用户自定义的消息处理逻辑。
3. 回调的核心传递与触发规律
(1)回调设置链(自上而下)
用户自定义业务回调 → TcpServer保存 → 新连接到来时传递给Connection → Connection绑定底层Channel的I/O回调,最终所有回调落地到Channel:
用户代码 → TcpServer → Connection → Channel
(2)回调触发链(自下而上)
epoll监听事件触发 → Channel执行底层I/O回调 → 组件(Acceptor/Connection)执行自身处理逻辑 → 触发上层业务回调 → 执行用户逻辑:
fd事件 → Channel → Acceptor/Connection → TcpServer → 用户代码
4. 核心回调一览表(精简)
| 回调所属 | 回调名称 | 设置方 | 触发时机 | 核心作用 |
|---|---|---|---|---|
| Channel | 四大I/O回调 | 持有Channel的组件 | fd对应I/O事件发生 | 触发组件的底层事件处理 |
| Acceptor | _new_conn_cb | TcpServer | accept成功后 | 创建新的Connection |
| Connection | _message_cb | TcpServer→用户 | 收到客户端数据 | 执行用户的消息处理逻辑 |
| Connection | _connected/_closed_cb | TcpServer→用户 | 连接建立/关闭 | 执行用户的连接管理逻辑 |
三、Channel与Connection的关系
核心破除误解
并非每个Channel都有Connection ,这是最常见的误区:Channel是通用的fd事件封装类,任何需要监听fd的组件都可以创建Channel;Connection只是"客户端conn_fd"这种特定场景的封装,其内部通过Channel处理conn_fd的I/O事件。
不同fd的Channel归属及Connection关联
| fd类型 | Channel持有者 | 是否有Connection | 应用场景 |
|---|---|---|---|
| eventfd | EventLoop | ❌ 无 | 跨线程唤醒EventLoop |
| timerfd | TimerWheel | ❌ 无 | 定时器超时事件 |
| listen_fd | Acceptor | ❌ 无 | 监听客户端新连接 |
| conn_fd | Connection | ✅ 有(自身) | 管理已建立的TCP连接 |
四、为什么Channel要通过EventLoop修改事件
Channel修改关注的事件(如开启/关闭可写监听)时,不能直接调用Poller的接口 ,必须通过EventLoop中转,核心原因是保证线程安全,同时兼顾解耦和接口统一。
1. 直接调用Poller的致命问题:数据竞争
epoll的核心接口epoll_ctl(修改事件)和epoll_wait(监听事件)不支持跨线程同时操作 ,若业务线程直接调用Poller修改事件,而EventLoop线程正在执行epoll_wait,会导致未定义行为:
Plain
线程A(业务):conn->EnableWrite() → Channel直接调用Poller::UpdateEvent() → epoll_ctl()
线程B(EventLoop):Poller::Poll() → epoll_wait()
→ 两个线程同时操作epoll,数据竞争!
2. 正确做法:通过EventLoop中转,保证线程安全
Channel将事件修改请求交给EventLoop,由EventLoop判断是否在当前线程,跨线程则投递到任务队列,确保所有epoll操作都在EventLoop线程执行:
C++
// Channel的事件修改接口,中转给EventLoop
void Channel::EnableWrite() {
_events |= EPOLLOUT;
_loop->UpdateEvent(this); // 不直接操作Poller
}
// EventLoop保证操作在当前线程执行
void EventLoop::UpdateEvent(Channel *channel) {
if (IsInLoopThread()) {
_poller.UpdateEvent(channel); // 同线程,直接执行
} else {
RunInLoop([this, channel]() { // 跨线程,投递任务
_poller.UpdateEvent(channel);
});
}
}
3. 其他重要原因
-
解耦设计:Channel只依赖EventLoop,不依赖Poller;Poller是EventLoop的内部实现,若后续替换为kqueue/io_uring,Channel无需修改;
-
接口统一:EventLoop对外提供统一的事件操作接口(Update/Remove),隐藏底层Poller的细节;
-
生命周期管理:Channel持有EventLoop指针,只要EventLoop存在,Poller就一定存在,避免操作空指针。
五、多线程架构:LoopThread 与 LoopThreadPool
为充分利用多核CPU,Reactor采用One Loop Per Thread 核心模型:每个线程运行一个独立的EventLoop,线程与EventLoop一一对应,由LoopThread实现"线程+EventLoop"的绑定,LoopThreadPool管理多个LoopThread,实现线程池功能。
1. LoopThread:单个线程+EventLoop的封装
核心作用:创建一个独立线程,在该线程内初始化EventLoop并启动事件循环,同时解决跨线程安全获取EventLoop指针的问题。
(1)核心代码
C++
class LoopThread {
private:
std::mutex _mutex;
std::condition_variable _cond;
EventLoop *_loop; // 指向线程内的EventLoop,唯一成员变量
std::thread _thread;
// 线程入口函数:在新线程内创建EventLoop并启动
void ThreadEntry() {
EventLoop loop; // 栈上创建EventLoop,与线程绑定
{
std::unique_lock<std::mutex> lock(_mutex);
_loop = &loop; // 保存EventLoop指针
_cond.notify_all(); // 唤醒主线程的等待
}
loop.Start(); // 启动事件循环,永不返回
}
public:
LoopThread() : _loop(nullptr), _thread(&LoopThread::ThreadEntry, this) {}
// 主线程获取EventLoop指针,阻塞等待直到指针初始化完成
EventLoop *GetLoop() {
std::unique_lock<std::mutex> lock(_mutex);
_cond.wait(lock, [&](){ return _loop != nullptr; }); // 等待_loop非空
return _loop;
}
};
(2)核心问题:跨线程同步获取EventLoop指针
EventLoop必须在所属线程内创建 (栈上),但主线程需要获取其指针,存在时序问题:主线程创建LoopThread后,新线程可能还未初始化EventLoop,直接获取会得到空指针。
解决方案 :互斥锁+条件变量 同步,主线程调用GetLoop()时阻塞,直到新线程完成EventLoop初始化并唤醒主线程。
(3)关键理解:_loop指针的唯一性
LoopThread的_loop是类的唯一成员变量 ,主线程和新线程访问的是同一个指针,该指针指向新线程栈上的EventLoop对象,因此需要同步保证指针赋值完成后主线程才能读取。
2. LoopThreadPool:管理多个LoopThread
核心作用:创建并管理一组LoopThread,为新连接轮询分配EventLoop,实现负载均衡。
(1)核心代码
C++
class LoopThreadPool {
private:
int _thread_count; // 工作线程数
int _next_idx; // 轮询索引,用于负载均衡
EventLoop *_baseloop; // 主线程的EventLoop(无工作线程时使用)
std::vector<EventLoop*> _loops; // 保存所有工作线程的EventLoop指针
public:
LoopThreadPool(EventLoop *baseloop) : _baseloop(baseloop), _next_idx(0) {}
// 创建指定数量的LoopThread,阻塞等待所有EventLoop初始化完成
void Create(int thread_count) {
_thread_count = thread_count;
for (int i = 0; i < thread_count; i++) {
LoopThread *t = new LoopThread();
_loops.push_back(t->GetLoop()); // 阻塞等待GetLoop()返回
}
}
// 轮询获取下一个EventLoop,实现负载均衡
EventLoop *NextLoop() {
if (_thread_count == 0) return _baseloop; // 无工作线程,用主线程的
EventLoop *loop = _loops[_next_idx];
_next_idx = (_next_idx + 1) % _thread_count; // 取模轮询
return loop;
}
};
(2)核心工作流程
-
TcpServer初始化时,创建LoopThreadPool并指定工作线程数;
-
LoopThreadPool创建对应数量的LoopThread,获取每个LoopThread的EventLoop指针并保存;
-
新连接到来时,TcpServer调用
NextLoop()轮询获取一个EventLoop,将新连接的Connection绑定到该EventLoop上; -
每个Connection的所有I/O事件都由绑定的EventLoop线程处理,实现线程间的负载均衡。
3. 整体多线程架构
Plain
TcpServer
│ 持有 LoopThreadPool(_pool)
├─────────────────────────────────────────────────┐
│ │
▼ ▼
LoopThread0 → EventLoop0 LoopThreadN → EventLoopN
(线程1,独立事件循环) (线程N+1,独立事件循环)
│ │
└── 处理分配的Connection1/Connection2 └── 处理分配的ConnectionN/ConnectionN+1
六、常见问题与注意事项
1. 头文件包含
使用C++多线程相关组件时,需单独包含对应头文件,避免编译错误:
C++
#include <mutex> // std::mutex、std::unique_lock
#include <condition_variable> // std::condition_variable(单独头文件!)
#include <thread> // std::thread
2. 栈对象 vs 堆对象创建EventLoop
LoopThread中采用栈上创建EventLoop,而非堆上,两者核心区别:
| 特性 | 栈上创建(EventLoop loop;) | 堆上创建(new EventLoop();) |
|---|---|---|
| 生命周期 | 与线程绑定,线程退出则析构 | 手动delete,否则内存泄漏 |
| 指针赋值 | _loop = &loop; | _loop = loop; |
| 调用方式 | loop.Start(); | loop->Start(); |
| 优势 | 无需手动管理,自动析构 | 生命周期灵活 |
3. 栈上EventLoop的生命周期安全
LoopThread中栈上创建EventLoop看似有"悬空指针"风险,但实际绝对安全 :因为loop.Start()是无限循环的事件循环 ,线程入口函数ThreadEntry()在loop.Start()后不会返回,因此栈上的EventLoop对象只要线程运行就一直存在 ,_loop指针不会悬空。
七、核心总结
1. 核心设计原则
-
单一职责:每个组件只做一件事(Channel封装fd、EventLoop驱动循环、Connection管理连接);
-
事件驱动:所有逻辑由fd事件/定时器事件触发,无主动轮询,性能高效;
-
线程安全:所有epoll操作、fd事件处理都在对应EventLoop线程执行,跨线程通过任务队列投递;
-
One Loop Per Thread:线程与EventLoop一一对应,充分利用多核,避免锁竞争。
2. 回调关系一句话总结
Channel是底层回调载体,谁创建Channel谁就设置其底层I/O回调;Acceptor/Connection/EventLoop等组件各自管理自己的Channel,用户的业务回调通过TcpServer传递给Connection,最终在Channel的I/O回调中触发,实现"底层驱动上层,上层自定义业务"。
3. Reactor架构的核心优势
-
高性能:基于epoll的I/O多路复用,支持高并发连接,无惊群问题;
-
高扩展:One Loop Per Thread模型可通过增加工作线程数线性扩展性能;
-
低耦合:组件职责清晰,依赖关系简单,易于维护和扩展(如替换Poller、新增定时器);
-
高易用:用户只需自定义业务回调,无需关心底层epoll、多线程、事件分发等细节。