从数据传输和网络框架的角度,常见高性能线程模型可以按”谁处理数据、谁响应事件、谁执行计算”的职责划分来归类。
1. Run-to-Completion(RTC)
一句话:一个线程拿到请求后从头处理到尾,不切换上下文。
epoll_wait → accept → read → 业务逻辑 → write → epoll_wait
│ │
└──────── 全在一个线程内搞定 ──────────────────┘
| 优点 | 缺点 |
|---|---|
| 零上下文切换,cache 热 | 单个请求阻塞会饿死后续请求 |
| 无锁,无共享状态 | 多核利用率差,需要多进程/多实例 |
| 代码简单线性 | 不能有 sleep / 慢 I/O |
代表:Seastar、DPDK、早期的 Redis(单线程)、Nginx worker(每个 worker 独立 RTC)。
2. Reactor / Proactor(事件驱动)
一句话:一个事件循环线程负责监听 IO,把就绪的 fd 分发给 handler。
Reactor(同步 IO)
main loop: handler:
epoll_wait ──→ fd 就绪 │
│ read() / 业务逻辑
├──→ dispatch(handler) (handler 自己读)
Proactor(异步 IO)
main loop: handler:
等待 OS 完成 IO ──→ 数据已在 buffer │
│ 直接用,无需再 read
└──→ dispatch(handler)
| 模式 | 谁读数据 | 典型实现 |
|---|---|---|
| Reactor | 应用自己 read | libevent、libuv、Netty |
| Proactor | OS 帮你读了 | IOCP(Windows)、io_uring(Linux) |
常见变体:单 reactor + 线程池(Netty 主从 reactor、memcached)。
3. Half-Sync / Half-Async
一句话:IO 层异步(reactor),业务逻辑层同步(线程池),中间用队列连接。
[异步层] [队列] [同步层]
reactor thread worker pool
│ │
epoll → 事件入队 ────────────→ 队列 ──→ worker 取出 → 业务逻辑
│
返回结果入队 → reactor 写出
| 优点 | 缺点 |
|---|---|
| IO 和计算解耦 | 队列是竞争点 |
| 业务逻辑写同步代码即可 | 多次上下文切换 |
| worker 池可伸缩 | 延迟增加 |
代表:Thrift、gRPC 的默认线程模型、早期的 muduo。
4. Leader-Follower
一句话:一组线程轮流当 leader 负责 accept/监听事件,有请求后变成 follower 去处理,另一个线程接班当 leader。
[Thread 1: Leader] ─→ epoll_wait ─→ 拿到请求 ─→ 变成 follower 处理
│
[Thread 2] ─→ 发现 leader 走了 ─→ 自己变成 leader ─→ epoll_wait
| 优点 | 缺点 |
|---|---|
| 无中心调度器,线程自治 | 所有线程共享一个 epoll fd 有惊群 |
| 请求直接由处理线程接收 | 实现复杂(状态机) |
代表:ACE(自适应通信环境)、部分 Java Reactor 框架。
5. Staged Event-Driven Architecture(SEDA)
一句话:把处理拆成多个 stage,每个 stage 有自己独立的线程池和队列,阶段间靠队列传递。
[accept stage] → 队列 → [decode stage] → 队列 → [dispatch stage] → 队列 → [reply stage]
线程池 A 线程池 B 线程池 C 线程池 D
每个 stage 可以独立调节线程数、队列长度、拒绝策略,天然支持背压。
| 优点 | 缺点 |
|---|---|
| 每阶段独立调优 | 多次跨线程+跨队列,延迟累加 |
| 天然管道化,可观察性好 | 队列深度设计不当会雪崩 |
| 背压内建 | 代码被队列割裂 |
代表:SEDA(Berkeley 论文)、Netty 的 Pipeline、Kafka broker 内部类似设计。
6. Work Stealing / Fork-Join
一句话:每个线程有自己的队列,干完了就去偷别人的活。
Thread A: [task | task | task] ────────────────→
Thread C: []
Thread B: [task | task] ───→ 干完了 ──→ steal ──→ [task]
| 优点 | 缺点 |
|---|---|
| 自动负载均衡 | steal 有开销(需要同步) |
| 无中心调度 | 长尾延迟不可控 |
| 递归分解任务天然适配 | 不适合 IO 密集型 |
代表:Java ForkJoinPool、Go scheduler(work stealing + GMP)、Tokio(Rust)、Intel TBB、Rayon。
7. M:N 协程 / 用户态调度
一句话:M 个协程跑在 N 个 OS 线程上,协程在用户态切换,阻塞不占线程。
Goroutine × 10000 ──→ scheduler ──→ OS Thread × 4
│
runtime 负责:
- work stealing
- 网络阻塞时自动 yield
- 抢占式调度(Go 1.14+)
| 平台 | 协程实体 | 调度方式 |
|---|---|---|
| Go | goroutine | 协作+有限抢占,runtime 调度 |
| Rust | async task | async/await + 编译器生成状态机 + 用户选择运行时 |
| C++ | coroutine | C++20,无栈协程 |
| Erlang | process | VM 级调度,抢占式,不共享内存 |
8. Submit-Then-Poll(你的场景)
一句话:异步提交请求到一个独立的数据面引擎,上层自己控制何时检查完成。
app thread: submit ──→ [队列/硬件] ──→ engine workers
←── poll status ←── atomic / CQ
| 和其他模型的关键区别 | 原因 |
|---|---|
| 引擎有自己的线程池 | 封装硬件细节(RDMA/CUDA/DPDK) |
| 上层不自带 event loop | 调度权留给应用框架 |
| 无回调 | 避免控制反转,方便批量化 |
| 无阻塞 | overlap 空间最大 |
代表:Mooncake TE、libibverbs 原生接口、CUDA stream + cudaLaunchHostFunc、NVMe 的 SPDK。
对比总览
| 模型 | IO 线程 | 计算线程 | 同步原语 | 适合场景 |
|---|---|---|---|---|
| RTC | 自处理 | 自处理 | 无 | 纯内存/非阻塞 IO |
| Reactor | 专用线程 | 专用线程 | 队列 | 通用网络服务 |
| Half-Sync/Half-Async | 专用线程 | 线程池 | 队列 | 同步逻辑的 IO 密集服务 |
| Leader-Follower | 轮班 | 轮班 | 无 | 请求处理均匀 |
| SEDA | 多级线程池 | 多级线程池 | 多级队列 | 复杂的管道化服务 |
| Work Stealing | N/A | 所有线程都是 | 窃取队列 | 计算密集/递归并行 |
| M:N 协程 | runtime 调度 | runtime 调度 | channel | 并发连接多、IO 等待多 |
| Submit-Then-Poll | 引擎线程 | 应用线程 | atomic/队列 | 硬件近端、数据面 |
现实中的系统通常是混搭。比如 Mooncake 的典型场景是:
- Submit-then-poll 负责数据面(RDMA 传输)
- Work stealing(如 Tokio/Go runtime)负责上层请求调度
- Reactor 负责 metadata 服务和 RPC
三种模型各管一层,通过队列/atomic 连起来。