服务器并发模型
服务器并发模型
并发模型决定服务器如何同时管理多个客户端连接和任务。它不是单纯的“使用线程”或“使用 epoll”,而是 I/O 等待方式、事件分发、业务执行、连接状态和资源控制的组合。
先区分两类工作
一个网络服务器通常同时面对两类工作:
| 工作 | 示例 | 特点 |
|---|---|---|
| I/O 工作 | accept()、recv()、send()、协议缓冲和连接关闭 |
通常很短,但会等待网络事件 |
| 业务工作 | 数据库查询、文件压缩、图片处理、调用外部服务 | 可能耗时,甚至阻塞很久 |
高并发设计的核心原则是:不要让耗时业务阻塞负责大量连接 I/O 的事件循环。事件循环应尽快完成收发和状态更新;耗时任务通常交给线程池、进程池或异步后端处理。
单线程阻塞模型
最简单的服务器在一个线程中顺序处理一个连接:
1 | accept 一个客户端 |
优点:
- 代码最直观,几乎没有共享状态和并发同步问题。
- 非常适合学习 Socket 基础、验证协议或编写临时工具。
缺点:
- 一个慢客户端或长连接会阻塞所有后续客户端。
- 吞吐量和连接并发能力都很弱。
它不适合真实的多客户端服务,但常作为理解其他模型的起点。
多进程模型
主进程负责监听和接受连接,子进程负责处理客户端。早期常见方式是 accept() 后 fork(),或预先创建多个工作进程。
1 | 主进程:listen / accept |
优点:
- 地址空间隔离较好,一个子进程崩溃通常不会直接破坏其他进程的内存。
- 能利用多核 CPU。
缺点:
- 创建进程、地址空间和进程间通信的成本通常高于线程。
- 共享连接状态、缓存和限流信息更复杂。
- 每连接
fork()在高并发下代价很大。
预派生(prefork)进程池可减少频繁创建进程的开销。多进程仍适用于强隔离需求、传统 Web 服务架构或通过多进程提升故障隔离的系统。
每连接一个线程
每当 accept() 返回新的 client_fd,就创建一个线程,以阻塞方式处理该连接:
1 |
|
优点:
- 编程模型接近顺序代码,每个连接的状态自然保存在自己的调用栈中。
- 慢客户端只会阻塞负责它的线程,不会阻塞其他连接。
缺点:
- 线程数量与连接数线性增长;大量空闲长连接也会占用线程栈和调度资源。
- 线程创建/销毁、上下文切换和内存占用会限制规模。
detach()使线程生命周期难以统一管理,示例中不推荐直接用于生产。
它可以用于连接数有限、每个连接逻辑简单的服务,但应设置最大连接数和线程上限。
线程池模型
线程池预先创建固定数量或有上限的一组工作线程,通过任务队列分配工作,避免“一连接一线程”的无限增长。
1 | 监听线程 / 事件循环 |
线程池常见的两种使用方式:
| 方式 | 做法 | 适用性 |
|---|---|---|
| 阻塞式连接任务 | 一个工作线程独占一个连接并阻塞读写 | 简单,但长连接仍可能耗尽线程 |
| 业务任务池 | I/O 由事件循环处理,完整请求或耗时工作交给线程池 | 更适合大量连接与耗时业务 |
线程池不是“线程越多越好”。CPU 密集型工作通常以 CPU 核数附近为起点;大量阻塞 I/O 的工作可以更多,但仍要通过压测和资源限制确定。任务队列必须有上限:当队列满时需要定义背压策略,例如拒绝请求、返回繁忙错误、暂停读取或丢弃可丢弃任务。
基于多路复用的事件驱动模型
一个事件循环使用 epoll、kqueue、poll 或 select 等接口等待多个连接的 I/O 事件。连接通常设置为非阻塞。
1 | EventLoop |
优点:
- 少量线程可管理大量大部分时间空闲的连接。
- 不会为每个连接保留一个阻塞线程。
- 便于统一处理超时、限流、连接统计和背压。
缺点:
- 状态机、缓冲区、错误路径和对象生命周期更复杂。
- 如果把 CPU 密集或阻塞业务放进事件循环,整个循环仍会卡住。
- 非阻塞
send()、部分发送、可写事件注册都需要正确管理。
I/O 多路复用接口与 epoll 的 LT/ET 细节见《IO多路复用》。
Reactor 模型
Reactor 是事件驱动服务器的常见组织方式:Reactor 等待并分发就绪事件,处理器在收到事件后自行执行实际 accept()、recv() 或 send()。
1 | Reactor / EventLoop |
常见 Reactor 组合:
| 结构 | I/O 与业务分工 | 适用情况 |
|---|---|---|
| 单 Reactor 单线程 | 一个线程处理 I/O、协议和业务 | 逻辑简单、负载较轻;业务必须很短 |
| 单 Reactor + 线程池 | Reactor 处理 I/O,线程池执行耗时业务 | 常见折中方案 |
| 主从 Reactor + 线程池 | 主 Reactor 接受连接;多个从 Reactor 分别管理连接 I/O;业务再进线程池 | 多核、高连接数服务 |
主从 Reactor 的常见流程:
1 | 主 Reactor 收到监听事件 |
一个连接应尽量由固定的事件循环拥有和修改其 I/O 状态,这称为连接所有权。工作线程不能随意直接修改该连接的事件注册或输出缓冲区;更稳妥的做法是将结果投递回所属事件循环,由它串行地更新连接状态。
Proactor 模型
Proactor 关注的是完成事件:应用先提交异步读写请求,操作系统完成真正的 I/O 后通知应用结果。
1 | 应用提交异步读/写请求 |
| 模型 | 通知的内容 | 谁调用实际读写 |
|---|---|---|
| Reactor | “fd 已就绪,可以读/写” | 应用在事件回调中调用 recv() / send() |
| Proactor | “此前提交的读/写已经完成” | 内核完成已提交的操作,应用消费结果 |
Windows IOCP 是典型 Proactor 风格接口;Linux io_uring 可构建完成队列驱动的架构。不要仅因使用了 epoll 就称为 Proactor:epoll 通常通知的是就绪,而不是读写完成。
常见混合架构
实际服务器常采用混合方案:
1 | 非阻塞 I/O + 多路复用(管理连接) |
这种设计既避免了海量连接对应海量线程,也避免了数据库、磁盘或 CPU 密集任务卡住事件循环。
选择与容量规划
| 情况 | 推荐起点 |
|---|---|
| 教学、小工具、连接很少 | 单线程阻塞模型 |
| 连接数有限且每连接逻辑简单 | 有上限的每连接线程 / 线程池 |
| 大量空闲长连接、短 I/O 操作 | 非阻塞 I/O + Reactor |
| 多核且高连接数、业务可并行 | 主从 Reactor + 有界业务线程池 |
| 需要平台原生完成队列能力 | IOCP、io_uring 等 Proactor 风格方案 |
无论使用哪种模型,都应明确设置和监控:最大连接数、fd 上限、线程数、任务队列长度、单连接输入/输出缓冲区上限、请求超时、空闲超时、日志与指标。高并发问题往往不是“事件循环不够快”,而是没有背压和资源边界。
常见误区
epoll不是服务器架构本身;它只是一个 I/O 事件通知机制。- 线程池不能自动让服务器高并发;如果任务会永久阻塞,固定线程池仍会被耗尽。
- 一个事件循环里不能执行耗时业务;否则所有归属该循环的连接都会受影响。
- 可写事件不代表可以一次
send()完全部响应;必须维护输出缓冲区和已发送位置。 - 多线程处理同一连接必须同步;更推荐将连接 I/O 状态限制在所属事件循环线程中。
- 优化前先压测和观测瓶颈;数据库、锁竞争、日志、序列化或下游服务常常比 I/O 模型更先成为问题。



