进程、线程、协程:后端服务到底是怎么处理并发请求的?
在上一篇 《理解并发:一个 HTTP 请求到后端到底发生了什么?》 我们从一个 HTTP 请求出发,理解了并发最基本的概念:
HTTP 请求
↓
Web Server
↓
后端服务
↓
CPU / IO
↓
MySQL / Redis / RPC
↓
HTTP 响应
但这里留下了一个非常重要的问题:
后端服务到底由谁来执行这些请求?
如果同时来了 1000 个 HTTP 请求,服务器是不是创建 1000 个进程?
如果不是,是不是创建 1000 个线程?
如果也不是,那么一个线程又是怎么处理大量请求的?
要回答这些问题,就必须理解后端程序最核心的三个执行概念:
进程
线程
协程
1. 先建立一个整体认识
可以先把三者理解成三个不同层次的概念:
进程
│
├── 线程
│ │
│ ├── 协程
│ ├── 协程
│ └── 协程
│
└── 线程
但这里有一个需要特别注意的地方:
协程并不是操作系统进程或线程的同级概念。
进程和线程主要属于操作系统调度与资源管理层面;协程通常是由运行时、语言、框架或用户态库提供的更轻量级执行机制。
因此:
进程
→ 操作系统资源隔离与管理单位
线程
→ 操作系统调度的执行单位
协程
→ 用户态的轻量级执行/挂起/恢复机制
具体实现会因语言和运行时不同而不同。
例如 Go 的 goroutine、Python 的 coroutine、Swoole 的 coroutine,并不能简单认为底层实现完全一样。
2. 什么是进程?
进程(Process)可以简单理解为:
一个正在运行的程序实例,以及它所拥有的一组运行资源。
例如启动一个 PHP-FPM Worker:
PHP-FPM
│
├── Worker Process 1
├── Worker Process 2
├── Worker Process 3
└── Worker Process 4
每个进程拥有相对独立的:
虚拟地址空间
文件描述符
进程状态
资源上下文
一个进程内部可以包含多个线程。
例如:
Process A
├── Thread 1
├── Thread 2
└── Thread 3
Process B
├── Thread 1
└── Thread 2
进程之间通常具有较强的资源隔离性。
因此,如果一个进程崩溃:
Process A ×
一般不会直接导致:
Process B
Process C
同时崩溃。
这也是多进程模型非常重要的优势之一。
3. 什么是线程?
线程(Thread)是操作系统进行 CPU 调度的重要执行单位。
一个进程可以包含多个线程:
Process
│
├── Thread A
├── Thread B
└── Thread C
同一个进程中的线程通常共享:
代码
堆
全局数据
打开的文件等进程资源
但每个线程拥有自己的:
栈
寄存器状态
程序执行位置
因此线程比进程轻量,创建和切换成本通常也低于进程,但线程之间共享资源也意味着:
线程安全问题更加重要。
例如两个线程同时修改共享数据:
Thread A ──┐
├── Shared Data
Thread B ──┘
如果没有正确同步,就可能产生:
竞态条件
数据竞争
死锁
因此常见的线程同步机制包括:
Mutex
Semaphore
Condition Variable
Read/Write Lock
Atomic Operation
4. 进程、线程和 HTTP 请求是什么关系?
现在重新回到 Web 服务。
假设服务器收到:
100 个 HTTP 请求
最简单的处理方式之一是:
Request 1 → Process 1
Request 2 → Process 2
Request 3 → Process 3
...
但如果真的为每个请求创建一个进程:
1000 Requests
↓
1000 Processes
通常代价太高。
所以实际系统会复用已经存在的执行资源。
例如多进程模型:
100 HTTP Requests
│
▼
Process Pool
┌─────┬─────┬─────┐
│ P1 │ P2 │ P3 │
└─────┴─────┴─────┘
或者线程池:
100 HTTP Requests
│
▼
Thread Pool
┌────┬────┬────┬────┐
│ T1 │ T2 │ T3 │ T4 │
└────┴────┴────┴────┘
又或者协程模型:
100 HTTP Requests
│
▼
Threads
│
┌────────┴────────┐
▼ ▼
Coroutine Coroutine
Coroutine Coroutine
Coroutine Coroutine
所以:
HTTP 请求和进程、线程、协程之间不是一一对应关系。
真正的对应关系取决于服务运行时采用的并发模型。
5. PHP-FPM:典型的多进程模型
PHP-FPM 是理解 PHP 并发模型非常好的例子。
典型架构:
Client
│
▼
Nginx
│
▼
PHP-FPM
│
├── Worker 1
├── Worker 2
├── Worker 3
└── Worker 4
PHP-FPM 会维护多个 PHP worker 进程。
当 HTTP 请求进入后:
Request
↓
Nginx
↓
PHP-FPM
↓
某个 Worker
↓
执行 PHP 代码
↓
返回结果
一个 worker 在处理一个请求时,通常会持续执行这个请求直到完成。
例如:
Worker 1 → Request A
Worker 2 → Request B
Worker 3 → Request C
Worker 4 → Request D
如果 4 个 worker 都在处理请求:
Request E
↓
等待
直到有 worker 空闲。
因此,PHP-FPM 的一个非常直观的并发模型是:
并发能力
≈
可用 Worker 数量
当然,真实系统还受到 CPU、IO、数据库连接、内存、Web Server 等因素影响,不能简单用 worker 数量定义系统最终吞吐能力。
6. 为什么传统线程模型也可以处理并发?
很多语言的 Web 服务会使用线程池。
例如:
HTTP Requests
│
▼
Thread Pool
│
┌─────┼─────┐
▼ ▼ ▼
T1 T2 T3
假设线程池有:
100 Threads
同时有:
1000 Requests
那么并不是 1000 个请求同时占用 1000 个线程。
可能是:
100 Requests
↓
100 Threads 执行
900 Requests
↓
Queue / 等待
一个线程处理:
Request A
↓
MySQL
↓
等待
如果采用传统阻塞式 IO,那么线程可能会在等待期间被阻塞。
这意味着:
Thread
↓
执行
↓
MySQL
↓
阻塞等待
线程依然被占用。
如果大量请求都在等待 IO:
100 Threads
↓
100 Threads 都在等待
新的请求就只能继续排队。
因此线程池大小是非常重要的资源参数。
7. 协程到底解决了什么问题?
协程的核心价值可以先用一句话概括:
让程序能够以更低的成本创建大量轻量级执行上下文,并在合适的时机主动挂起和恢复。
假设一个请求:
Request A
↓
查询 MySQL
↓
等待
如果是传统阻塞线程模型:
Thread A
↓
MySQL
↓
阻塞
而在支持协程和异步 IO 的运行时中,可以实现:
Coroutine A
↓
MySQL
↓
挂起
↓
Event Loop
↓
处理 Coroutine B
↓
处理 Coroutine C
↓
处理 Coroutine D
↓
MySQL 返回
↓
恢复 Coroutine A
于是:
一个线程
↓
Coroutine A
Coroutine B
Coroutine C
Coroutine D
...
就可以同时管理大量 IO 密集型任务。
这里最关键的不是“协程比线程快”这么简单。
真正重要的是:
协程让大量 IO 等待可以被更高效地组织和调度。
8. Swoole / Hyperf 的并发模型
对于 PHP 开发者来说,这也是最值得理解的一部分。
传统 PHP-FPM:
Nginx
↓
PHP-FPM
↓
多个 Worker Process
↓
执行 PHP 请求
而 Swoole/Hyperf 的运行方式不同。
可以粗略理解为:
HTTP Server
│
▼
Event Loop
│
┌──────────┼──────────┐
▼ ▼ ▼
Coroutine Coroutine Coroutine
│ │ │
▼ ▼ ▼
MySQL Redis HTTP
│ │ │
└──────────┴──────────┘
│
▼
Event Loop
当协程执行到支持协程化的 IO 操作时:
Coroutine A
↓
IO
↓
挂起
运行时可以让其他协程继续执行:
Coroutine A → 等待 IO
Coroutine B → 执行业务
Coroutine C → 查询 Redis
Coroutine D → 处理 HTTP 请求
IO 完成之后:
Coroutine A
↓
恢复执行
因此,在 IO 密集型 Web 服务中,协程模型可以显著减少大量线程阻塞等待所带来的资源浪费。
但必须强调:
协程并不会凭空增加 CPU 计算能力。
如果代码一直执行 CPU 密集型任务:
while (true) {
// 大量 CPU 计算
}
那么即使使用协程,也不会因为“用了协程”就自动获得更高性能。
因为:
CPU 计算
↓
持续占用执行线程
↓
其他协程也无法凭空获得更多 CPU
所以协程最适合解决的是:
大量 IO 等待
而不是:
无限 CPU 计算
9. Node.js 为什么一个线程也能处理很多请求?
Node.js 是另一个非常典型的例子。
它的核心模型可以简化成:
HTTP Requests
│
▼
Event Loop
│
┌────┼────┐
▼ ▼ ▼
A B C
Node.js 使用事件循环处理大量网络 IO。
例如:
const data = await db.query(...);
在异步 IO 模型下,等待数据库期间并不需要让 JavaScript 主线程一直“卡住”。
可以理解为:
Task A
↓
DB Query
↓
等待
↓
Event Loop
↓
Task B
↓
Task C
当数据库返回:
DB Response
↓
Event Loop
↓
继续执行 Task A
这就是事件驱动并发模型。
不过“Node.js 是单线程”也是一个容易误解的说法。
更准确地说:
Node.js 的 JavaScript 执行通常以单个主线程上的 Event Loop 为核心,但 Node.js 进程本身并不等于只有一个操作系统线程。
Node.js 运行时还可能涉及其他线程,例如处理某些异步任务的线程池。
因此不能把:
Node.js = 一个线程
当成严格的运行时实现描述。
10. 三种模型应该怎么选择?
现在可以把三种模型放在一起:
| 模型 | 基本执行单位 | 并发特点 | 典型场景 |
|---|---|---|---|
| 多进程 | Process | 隔离性强 | PHP-FPM |
| 多线程 | Thread | 共享内存、并发执行 | Java/.NET 等 |
| 协程 | Coroutine | 用户态轻量调度 | Swoole/Hyperf、部分异步运行时 |
更准确地说,现代 Web 服务往往不是简单的“三选一”。
实际架构通常是组合:
多进程
↓
多线程
↓
Event Loop
↓
协程
↓
异步 IO
例如一个服务可能是:
多个进程
│
├── Event Loop
│ ├── Coroutine A
│ ├── Coroutine B
│ └── Coroutine C
│
└── Event Loop
├── Coroutine D
├── Coroutine E
└── Coroutine F
因此讨论“哪个模型最好”通常没有意义。
真正应该问的是:
当前业务的并发模式是什么?CPU 密集还是 IO 密集?请求量是多少?下游资源是什么?运行时提供什么能力?
例如:
CPU 密集型
更应该关注:
CPU 核心数
进程/线程并行度
任务拆分
计算效率
IO 密集型
更应该关注:
异步 IO
IO 多路复用
协程
连接池
超时
下游服务容量
总结
理解进程、线程和协程,可以建立这样一个模型:
进程
↓
提供资源隔离
↓
线程
↓
承担操作系统级执行
↓
协程
↓
在用户态组织大量轻量级任务
↓
IO 多路复用 / Event Loop
↓
高效处理大量 IO 等待
它们解决的问题并不完全相同。
进程重点解决资源隔离和独立运行问题。
线程是操作系统重要的执行和调度单位,同一进程中的线程可以共享大量资源。
协程提供更加轻量的执行上下文和挂起/恢复机制,特别适合组织大量 IO 密集型任务。
对于 Web 后端,可以简单形成这样的认知:
PHP-FPM
→ 多进程
Java / .NET
→ 线程池 + 异步 IO 等模型
Node.js
→ Event Loop + 非阻塞 IO
Swoole / Hyperf
→ Event Loop + 协程 + 非阻塞 IO
但这些只是便于理解的概括,并不是完整的运行时实现描述。
最重要的是不要形成下面这些错误认识:
❌ 一个 HTTP 请求 = 一个线程
❌ 一个 HTTP 请求 = 一个进程
❌ 协程 = 更快的 CPU
❌ Node.js = 整个程序只有一个线程
❌ 协程可以解决所有高并发问题
更准确的理解是:
后端并发模型,本质上是在决定“如何承载、调度和执行大量同时存在的任务”。