OTOL设计模式 One Thread One Loop

@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 里。

要升级成多线程版,需要:

  1. 建 N 个 EventLoop(各跑一个线程)。

  2. 每个 EventLoop 有自己的 epollwakeupFd

  3. accept 新连接后,用 round-robin 选一个 EventLoop,把 Channel 投递过去(通过任务队列 + 唤醒)。

  4. 连接之后的 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 分配。

相关推荐
xiaoye-duck1 小时前
《Linux 网络编程》深入理解 IP 协议(一):网络层基础与 IP 协议头详解
linux·网络·ip
Wang's Blog1 小时前
Java 接入Redis: Redis简介与NoSQL定位
java·服务器·redis
ao-weilai1 小时前
Linux网络编程:Linux Socket TCP
linux·服务器·网络
是个西兰花1 小时前
UDP套接字编程
运维·服务器·网络
其实防守也摸鱼2 小时前
堆叠注入(Stacked Injection)详解
服务器·数据库·windows·https·ssl
郝学胜-神的一滴2 小时前
C++20模板元编程 05:吃透变参模板,解锁编译期万能参数能力
服务器·开发语言·c++·windows·vscode
Wang's Blog3 小时前
Java 接入Redis: 五大数据类型与存储结构选型
java·服务器·redis
Wang's Blog3 小时前
Java 接入Redis: 字符串与哈希类型操作命令
java·服务器·redis
菜根Sec5 小时前
没钱考网络安全证书了
网络·网络安全·信息安全·信息安全工程师·网络安全公司