@bit::Shadow
✧(≖ ◡ ≖✿
目录
[1. 单线程版(最基础)](#1. 单线程版(最基础))
[2. 多线程版(One Loop Per Thread)](#2. 多线程版(One Loop Per Thread))
[1. 一个连接只属于一个线程](#1. 一个连接只属于一个线程)
[2. 跨线程通信:任务队列 + eventfd](#2. 跨线程通信:任务队列 + eventfd)
[3. eventfd 的作用](#3. eventfd 的作用)
[1. 负载不均](#1. 负载不均)
[2. 跨线程操作有开销](#2. 跨线程操作有开销)
[3. 线程数不是越多越好](#3. 线程数不是越多越好)
[4. 主线程 accept 可能成瓶颈](#4. 主线程 accept 可能成瓶颈)
[七、和你的 epoll 项目的关系](#七、和你的 epoll 项目的关系)
One Thread One Loop(单线程单事件循环) 是 Reactor 模式的一种经典实现,指每个线程运行一个独立的事件循环(event loop),处理分配给它的所有 IO 事件。muduo、libevent、Node.js 等都用这个模型。下面系统介绍。
一、核心思想
一句话:一个线程,一个循环,循环里用 epoll 等 IO 多路复用处理事件。
text
Thread 1: while (running) { epoll_wait(); 处理事件; }
Thread 2: while (running) { epoll_wait(); 处理事件; }
...
每个线程有自己的 epoll 实例、自己的事件循环,互不干扰。线程之间通过任务队列 + 唤醒机制通信。
二、典型结构
1. 单线程版(最基础)
cpp
class EventLoop {
int epfd_;
bool running_;
public:
void Loop() {
while (running_) {
int n = epoll_wait(epfd_, events_, MAX, -1);
for (int i = 0; i < n; ++i) {
// 分发事件到对应的 handler
Dispatch(events_[i]);
}
DoPendingTasks(); // 处理跨线程投递的任务
}
}
};
一个线程跑一个 EventLoop::Loop(),所有 IO 事件在这个循环里处理。
2. 多线程版(One Loop Per Thread)
主线程(MainReactor)负责 accept,工作线程(SubReactor)各自跑一个 loop 处理连接 IO:
text
Main Thread (MainReactor)
└── EventLoop
├── 监听 listenfd
└── accept 新连接 → 分发给某个 SubReactor
Thread 1 (SubReactor) Thread 2 (SubReactor)
└── EventLoop └── EventLoop
├── conn fd 1 ├── conn fd 3
├── conn fd 2 ├── conn fd 4
└── ...
每个连接固定绑定到一个 SubReactor 线程,之后它的所有读写都在那个线程的 loop 里处理。
三、关键设计点
1. 一个连接只属于一个线程
连接创建时就被分配(round-robin、随机、按负载)到某个 SubReactor,之后所有 IO 都在该线程处理。好处:
-
无锁:连接的状态只被一个线程访问,不用加锁。
-
缓存友好:同一连接的操作在同一个 CPU 上。
-
避免竞态:不会两个线程同时读同一个 fd。
2. 跨线程通信:任务队列 + eventfd
一个线程想操作另一个线程管理的连接,不能直接操作,而是投递任务:
cpp
void EventLoop::RunInLoop(Func cb) {
if (IsInLoopThread()) {
cb(); // 本线程,直接执行
} else {
QueueInLoop(cb); // 跨线程,入队并唤醒
}
}
void EventLoop::QueueInLoop(Func cb) {
{
std::lock_guard<std::mutex> lock(mutex_);
pendingTasks_.push(std::move(cb));
}
Wakeup(); // 写 eventfd,唤醒目标线程的 epoll_wait
}
目标线程在 epoll_wait 被唤醒后,执行 DoPendingTasks()。
唤醒机制 用 eventfd(Linux)或 pipe:往 eventfd 写 8 字节,目标线程的 epoll 就返回,然后处理任务队列。
3. eventfd 的作用
cpp
wakeupFd_ = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
// 注册到 epoll,关心 EPOLLIN
-
需要唤醒时:
write(wakeupFd_, &one, 8)。 -
被唤醒后:
read(wakeupFd_, &one, 8)清空。
这样解决了 epoll_wait 阻塞时无法及时处理跨线程任务的问题。
四、完整组件图
text
┌─────────────────────────────────────────────┐
│ EventLoop (每线程一个) │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Epoller │ │ PendingTasks │ │
│ │ (epoll fd) │ │ + mutex │ │
│ └─────────────┘ └──────────────┘ │
│ ┌─────────────┐ │
│ │ wakeupFd │ ← 跨线程唤醒 │
│ │ (eventfd) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────┘
▲ ▲
│ 注册 fd │ 投递任务
┌────┴────┐ ┌────┴────┐
│ Channel │ │ 其他线程 │
└─────────┘ └─────────┘
-
EventLoop:核心循环。
-
Epoller:封装 epoll。
-
Channel:fd + 关心的事件 + 回调。
-
PendingTasks:跨线程任务队列。
-
wakeupFd:唤醒机制。
五、优点
| 优点 | 说明 |
|---|---|
| 无锁 | 连接只属于一个线程,状态无需加锁 |
| 简单 | 每个线程逻辑独立,容易推理 |
| 可扩展 | 线程数 ≈ CPU 核数,充分利用多核 |
| 缓存友好 | 同连接操作集中在一个 CPU |
| 避免惊群 | 每个 epoll 实例独立,不会多线程抢同一 fd |
六、缺点与注意
1. 负载不均
如果某些连接特别活跃,绑定它的线程会忙,其他线程闲。
缓解:用 round-robin 分配新连接,通常够用。
2. 跨线程操作有开销
投递任务要走队列 + eventfd 唤醒,比直接调用慢。
缓解:尽量减少跨线程操作,连接相关的都放本线程。
3. 线程数不是越多越好
线程数一般 = CPU 核数。太多线程上下文切换开销大,收益递减。
4. 主线程 accept 可能成瓶颈
高并发短连接场景,accept 本身可能成为瓶颈。
缓解 :多线程 accept(SO_REUSEPORT + 多个监听 socket),或主线程只 accept 不分发。
七、和你的 epoll 项目的关系
你的项目其实是 One Thread One Loop 的简化版:
text
Main Thread
└── TCPServer
├── Epoller
├── Listener (监听 fd)
└── Connection map (所有连接)
目前是单线程单 loop:所有 accept 和连接 IO 都在一个线程的 loop 里。
要升级成多线程版,需要:
-
建 N 个
EventLoop(各跑一个线程)。 -
每个 EventLoop 有自己的
epoll和wakeupFd。 -
accept 新连接后,用 round-robin 选一个 EventLoop,把
Channel投递过去(通过任务队列 + 唤醒)。 -
连接之后的 IO 都在那个 EventLoop 里处理。
这也是 muduo 的 EventLoopThreadPool 做的事。
八、和其他模型对比
| 模型 | 说明 | 代表 |
|---|---|---|
| One Thread One Loop | 每线程一个 loop,连接绑定线程 | muduo、libevent |
| 单 Reactor 单线程 | 一个线程处理所有 | Redis(6.0 前) |
| 多 Reactor 多线程 | 主从 Reactor | Nginx、muduo 多线程 |
| Proactor | 异步 IO,内核完成通知 | Windows IOCP、Boost.Asio |
| 协程 | 用户态调度,同步写法 | Go、C++20 协程 |
One Thread One Loop 是 Reactor 多线程化的基础单元,很多模型是它的组合。
九、一句话总结
One Thread One Loop = 每个线程跑一个独立的事件循环(epoll),连接固定绑定到某个线程,线程内无锁,跨线程通过任务队列 + eventfd 唤醒通信。 它是 Reactor 多线程化的核心,优点是简单、无锁、可扩展,缺点是负载可能不均、跨线程有开销。你的 epoll 项目目前是它的单线程形态,升级方向就是多开几个 EventLoop 各自跑 loop,连接 round-robin 分配。