四、OneThreadOneLoop 扩展
单 Reactor 虽然简洁,但所有 I/O 和业务处理都在一个线程中完成,无法充分利用多核 CPU。
One Thread One Loop模式通过让每个线程或进程运行一个独立的事件循环来解决这个问题,是从单 Reactor 向高并发架构扩展的自然路径。
4.1 OTOL 核心思想
每个线程/进程维护自己独立的事件循环和 epoll 实例,彼此互不干扰。
每个 Reactor 独立管理自己的 socket 完整生命周期,不涉及 I/O 穿插或乱序问题。
各 Reactor 管理的连接和文件描述符不重复,避免竞争。
只需将单 Reactor 写好,扩展为多进程或多线程的成本很低。
4.2 多进程方案
多进程方案中,Master 进程负责监听 listen socket,通过管道将新连接或就绪事件分发给多个 Slaver 子进程。
方案一采用 Listener 模块方式
Master 只负责监听 listen socket,给 connection 设置的回调函数仅是负载均衡地向管道发送就绪事件;Slaver 把管道读端封装为 Connection 加入自己的 Reactor,回调方法是 Accepter,由 Slaver 自行 accept 新连接。
方案二将 accept 动作上移到 Master
Master 监听 listen socket,在回调中一轮获取全部新连接的 sockfd,然后将所有 fd 按负载均衡策略投递到指定管道;Slaver 从管道读取所有 sockfd,直接添加到自己的 Reactor 中进行 I/O 处理。
这种方式更简单,因为多线程可以共享文件描述符,只需修改 connection 的回调方法即可。
从管道读取 sockfd 时,按 4 字节固定长度读取,自动完成序列化和反序列化。
多进程间也可以定义多个管道,用 vector 管理一组 pipefd 结构。
4.3 多线程方案一
管道通信
管道不仅能用于进程间通信,同样适用于线程间。
多线程方案一与多进程方案结构类似,Master 线程通过管道向 Worker 线程传递数据或事件。
下面是一个管道线程间通信的基础示例:
#include <iostream> #include <thread> #include <unistd.h> #include <cstring> int pipefds[2]; void worker_thread() { char buffer[256]; int n; while (true) { n = read(pipefds[0], buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = '\0'; std::cout << "Worker received: " << buffer << std::endl; } else if (n == 0) { std::cout << "No more data. Exiting." << std::endl; break; } else { std::cerr << "Error reading from pipe." << std::endl; break; } } } int main() { if (pipe(pipefds) == -1) { perror("pipe"); return 1; } std::thread worker(worker_thread); const char* messages[] = {"Hello!", "Another msg.", "Goodbye!"}; for (const char* msg : messages) { sleep(1); if (write(pipefds[1], msg, strlen(msg)) < 0) { perror("write"); break; } std::cout << "Main sent: " << msg << std::endl; } worker.join(); close(pipefds[0]); close(pipefds[1]); return 0; }4.4 多线程方案二
eventfd 驱动
如果不想使用管道,可以选择 eventfd 作为线程间事件通知机制。
方案二中,Master 线程负责监听 listen socket 并 accept 新连接,将获取到的所有 sockfd 存入一个共享队列,然后通过 eventfd 通知某个 Slaver 线程。
Slaver 线程的 Reactor 监听 eventfd 的读端,回调触发后从共享队列中取出所有新 sockfd,添加到自己的 Reactor 中。
流程分为五步:Master accept 新连接并将 fd 入队 → Master 通过 eventfd 通知 Slaver → Slaver 的 eventfd 就绪 → Slaver 回调执行,从队列中获取所有 sockfd → 将新 sockfd 添加到自己的 Reactor 中。
五、Eventfd 事件通知机制
5.1基本原理
eventfd 是 Linux 提供的一种轻量级事件通知机制,本质上是一个由内核维护的 64 位计数器,以文件描述符的形式呈现。
它可以与 epoll 等 I/O 多路复用机制结合使用:write 操作增加计数器值,read 操作读取并减少计数器值。
#include <sys/eventfd.h> int eventfd(unsigned int initval, int flags);5.2 两种工作模式
eventfd 有两种工作模式,区别在于 read 时计数器的减少方式:
模式 | read 行为 | 适用场景 |
|---|---|---|
普通模式 | 一次 read 将计数器值全部读出并清零 | 仅需事件通知,不关心具体计数 |
EFD_SEMAPHORE | 每次 read 计数器减 1,返回值恒为 1 | 需要信号量语义,精确控制消费次数 |
下面的代码分别写入 1 和 2(计数器累计为 3),然后连续读取三次,对比两种模式的输出差异:
eventfd 两种模式对比测试
int main() { // 模式1:普通模式,不设置 EFD_SEMAPHORE int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); // 模式2:信号量模式 // int efd = eventfd(0, EFD_SEMAPHORE | EFD_NONBLOCK | EFD_CLOEXEC); uint64_t value; value = 1; write(efd, &value, sizeof(value)); // 写入1 value = 2; write(efd, &value, sizeof(value)); // 写入2 value = 0; read(efd, &value, sizeof(value)); printf("Read value: %llu\n", (unsigned long long)value); value = 0; read(efd, &value, sizeof(value)); printf("Read value: %llu\n", (unsigned long long)value); value = 0; read(efd, &value, sizeof(value)); printf("Read value: %llu\n", (unsigned long long)value); close(efd); return 0; }普通模式下,第一次 read 就把累计值 3 全部读出,计数器清零,后续 read 返回 0:
模式1(普通模式)输出
Wrote 1 to eventfd Wrote 2 to eventfd Read value: 3 (counter is now 2) Read value: 0 (counter is now 1) Read value: 0 (counter is now 0)信号量模式下,每次 read 计数器减 1,返回值恒为 1,三次 read 分别消费一个计数:
模式2(EFD_SEMAPHORE)输出
Wrote 1 to eventfd Wrote 2 to eventfd Read value: 1 (counter is now 2) Read value: 1 (counter is now 1) Read value: 1 (counter is now 0)在 Reactor 扩展中,eventfd 仅用于事件通知,不传递具体消息内容,因此选择普通模式即可。
5.3 特点与注意事项
主要特点:
低开销:内部是一个 64 位计数器,内核维护成本低。
支持多路复用:可与 epoll、poll、select 结合使用。
原子性:读写操作是原子的,适合高并发场景。
高效性:相比传统管道,避免了多次数据拷贝,内核开销更小。
广播通知:可用于多对多的事件通知,不限于点对点通信。
注意事项:
eventfd 仅用于事件通知,不能传递具体的消息内容。
建议设置 EFD_NONBLOCK,避免阻塞操作。
需要信号量语义时设置 EFD_SEMAPHORE,每次读取计数器减 1。
使用完毕后记得关闭文件描述符,防止资源泄漏。
高并发场景下建议结合 epoll 使用,充分发挥事件驱动优势。
六、总结
Reactor 模式通过事件循环 + I/O 多路复用 + 回调分发,在单线程内实现了高并发网络处理。
在处理流程中,ET 模式下读写都需要循环到 EAGAIN;业务处理遵循"提取报文 → 反序列化 → 业务计算 → 序列化 → 封装报文 → 写入发送缓冲区"的六步流水线。
掌握单 Reactor 的完整实现后,向 OTOL 扩展只需关注连接分发和事件通知机制,核心的 Reactor 处理逻辑无需改动,这也正是 Reactor 模式可扩展性的体现。