从 CPU 到 FastAPI:一次讲清进程、线程、协程和 IO 多路复用
今天讨论的核心不是背概念,而是建立一条完整链路: 硬件并行能力 → 操作系统调度 → 用户态运行时 → 语言里的 async/await → 后端工程选型。
1. 进程、线程、协程分别是什么?
它们不是同一层的东西。进程和线程更多属于操作系统层,协程更多属于语言运行时 / 框架层。
进程:资源分配单位
一个正在运行的程序实例。拥有独立内存空间、文件句柄、堆、栈、权限等资源。 隔离性强,创建和切换成本高。
线程:CPU 调度单位
线程存在于进程内部,是实际执行代码的单位。线程共享同一个进程的内存和资源, 因此需要考虑锁、竞态、死锁。
协程:用户态轻量任务
协程是可以暂停和恢复的函数。一般由事件循环调度,遇到 await 主动让出执行权。 特别适合高并发 IO。
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度者 | 操作系统 | 操作系统 | 事件循环 / 运行时 |
| 切换成本 | 高 | 中 | 低 |
| 内存关系 | 默认隔离 | 同进程内共享 | 同线程内共享 |
| 适合场景 | CPU 密集、模型推理、任务隔离 | 阻塞 IO、调用同步 SDK、后台任务 | 高并发 IO、HTTP、数据库、WebSocket |
2. 为什么会出现协程?
协程的出现不是为了取代线程,而是为了解决“线程太重、回调太乱”的问题。
没有协程时
socket 可读时:
读取请求
发起数据库查询
注册数据库回调
数据库返回时:
处理数据
注册 socket 可写事件
socket 可写时:
发送响应
有协程后
async def handle_request():
request = await read_request()
data = await query_database(request)
await send_response(data)
3. 线程有竞争,协程也会有吗?
会。但线程是抢占式调度,协程通常是协作式调度,所以竞争出现的位置不同。
线程竞争
可能在几乎任何时刻被操作系统切走。
协程竞争
通常发生在 await / yield 这种让出点附近。
线程竞争示意
counter = 0
线程 A:读取 counter = 0
线程 B:读取 counter = 0
线程 A:写回 counter = 1
线程 B:写回 counter = 1
本来加了两次,结果还是 1。
线程共享内存,且可能被操作系统在任意指令附近抢占,所以需要 mutex、atomic、condition_variable 等机制。
协程竞争示意
counter = 0
async def add():
global counter
temp = counter
await asyncio.sleep(0)
counter = temp + 1
如果两个协程都在 await 前读到 counter = 0,await 后再写回,就会丢失更新。 所以协程也要避免在修改共享状态中间 await,必要时使用 asyncio.Lock。
4. C++ 和 Python 创建进程、线程、协程有什么不同?
C++ 更接近底层,控制力强;Python 封装更高,易用但受解释器和 GIL 影响。
Python 进程
from multiprocessing import Process
def worker():
print("子进程运行")
if __name__ == "__main__":
p = Process(target=worker)
p.start()
p.join()
Python 线程
import threading
def worker():
print("线程运行")
t = threading.Thread(target=worker)
t.start()
t.join()
Python 协程
import asyncio
async def worker():
await asyncio.sleep(1)
print("协程运行")
asyncio.run(worker())
C++ 进程
// Linux / Unix
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
} else {
// 父进程
}
}
C++ 线程
#include <thread>
void worker() {
// do something
}
int main() {
std::thread t(worker);
t.join();
}
C++20 协程
// 伪代码,依赖库封装 task
task<int> foo() {
int value = co_await async_read();
co_return value;
}
| 项目 | Python | C++ |
|---|---|---|
| 进程 | multiprocessing 封装较高,数据常需 pickle 序列化 | fork/exec 或 CreateProcess,更底层、更灵活 |
| 线程 | threading 是 OS 线程,但 CPython 受 GIL 影响 | std::thread/std::jthread 可真正多核并行 |
| 协程 | asyncio 提供事件循环和高层 API | C++20 提供语言机制,但调度通常依赖库 |
5. select、poll、epoll 和协程有什么关系?
它们不是协程,但经常作为异步 IO 的底层基础。
程序员看到的语言语法
可以暂停和恢复的任务
调度协程和 IO 事件
操作系统层面的 IO 多路复用机制
select
比较老,每次需要扫描一批 fd,数量和效率都有局限。像老师每次点完整个班名。
poll
相比 select 没有固定 fd 数量限制,但仍然需要线性扫描。名单变长了,但还是要扫。
epoll
Linux 高性能 IO 多路复用机制。谁准备好了就通知谁,适合大量连接。
6. 用户态和内核态
理解用户态/内核态,可以解释为什么线程切换比协程切换更重。
用户态
普通应用程序运行的低权限区域。Python、C++ 程序、浏览器、VSCode 平时都在用户态。 不能直接操作硬件、物理内存、磁盘、网卡。
内核态
操作系统内核运行的高权限区域。负责进程调度、线程调度、内存管理、文件系统、网络协议栈和硬件访问。
系统调用链路
7. CPU 的 x 核 x 线程和软件线程是什么关系?
CPU 核心/线程是硬件概念,进程/线程是软件概念。
示例:4 核 8 线程 CPU
下面每个物理核心有两个硬件线程。操作系统会把大量软件线程调度到这些硬件线程上运行。
8. 今天有代表性的问题
这些问题背后,正好串起了并发、调度、语言实现和底层 IO。
9. 小测验
点一下选项,看看今天的核心概念有没有串起来。