进程、线程、协程与 IO 多路复用学习总结

从 CPU 到 FastAPI:一次讲清进程、线程、协程和 IO 多路复用

今天讨论的核心不是背概念,而是建立一条完整链路: 硬件并行能力 → 操作系统调度 → 用户态运行时 → 语言里的 async/await → 后端工程选型。

进程是资源单位 线程是执行单位 协程是用户态轻量任务 epoll 是底层 IO 多路复用 GIL 限制 Python 字节码并行

1. 进程、线程、协程分别是什么?

它们不是同一层的东西。进程和线程更多属于操作系统层,协程更多属于语言运行时 / 框架层。

Process

进程:资源分配单位

一个正在运行的程序实例。拥有独立内存空间、文件句柄、堆、栈、权限等资源。 隔离性强,创建和切换成本高。

Thread

线程:CPU 调度单位

线程存在于进程内部,是实际执行代码的单位。线程共享同一个进程的内存和资源, 因此需要考虑锁、竞态、死锁。

Coroutine

协程:用户态轻量任务

协程是可以暂停和恢复的函数。一般由事件循环调度,遇到 await 主动让出执行权。 特别适合高并发 IO。

维度 进程 线程 协程
调度者 操作系统 操作系统 事件循环 / 运行时
切换成本
内存关系 默认隔离 同进程内共享 同线程内共享
适合场景 CPU 密集、模型推理、任务隔离 阻塞 IO、调用同步 SDK、后台任务 高并发 IO、HTTP、数据库、WebSocket

2. 为什么会出现协程?

协程的出现不是为了取代线程,而是为了解决“线程太重、回调太乱”的问题。

多进程 隔离强,但太重
多线程 轻一些,但锁复杂
事件驱动 高效,但回调乱
协程 轻量 + 顺序代码
async/await 异步写得像同步

没有协程时

socket 可读时:
    读取请求
    发起数据库查询
    注册数据库回调

数据库返回时:
    处理数据
    注册 socket 可写事件

socket 可写时:
    发送响应

有协程后

async def handle_request():
    request = await read_request()
    data = await query_database(request)
    await send_response(data)
协程的核心价值:等待 IO 时主动让出执行权,让一个线程可以管理大量等待中的任务,同时保持代码结构清晰。

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 的底层基础。

async / await
程序员看到的语言语法
上层抽象
把异步流程写成接近同步的样子。例如:先 await 读取请求,再 await 查询数据库,再 await 返回响应。
协程 Coroutine
可以暂停和恢复的任务
用户态任务
协程执行到 await 时暂停,把控制权交还给事件循环,等 IO 就绪后再恢复。
事件循环 Event Loop
调度协程和 IO 事件
运行时调度
事件循环负责管理哪些协程等待哪些 IO,哪个 IO 准备好了,就恢复对应协程。
select / poll / epoll / kqueue / IOCP
操作系统层面的 IO 多路复用机制
内核能力
它们帮助一个线程同时监听多个文件描述符或 socket。Linux 下高并发网络服务常用 epoll。

select

比较老,每次需要扫描一批 fd,数量和效率都有局限。像老师每次点完整个班名。

poll

相比 select 没有固定 fd 数量限制,但仍然需要线性扫描。名单变长了,但还是要扫。

epoll

Linux 高性能 IO 多路复用机制。谁准备好了就通知谁,适合大量连接。

6. 用户态和内核态

理解用户态/内核态,可以解释为什么线程切换比协程切换更重。

User Mode

用户态

普通应用程序运行的低权限区域。Python、C++ 程序、浏览器、VSCode 平时都在用户态。 不能直接操作硬件、物理内存、磁盘、网卡。

Kernel Mode

内核态

操作系统内核运行的高权限区域。负责进程调度、线程调度、内存管理、文件系统、网络协议栈和硬件访问。

系统调用链路

用户代码 read / write / socket
系统调用 syscall
进入内核态 OS 执行高权限操作
返回结果 文件 / 网络 / 进程
回到用户态 程序继续执行
协程切换通常发生在用户态,所以很轻;线程切换由操作系统调度,往往涉及内核调度和上下文切换,所以更重。

7. CPU 的 x 核 x 线程和软件线程是什么关系?

CPU 核心/线程是硬件概念,进程/线程是软件概念。

示例:4 核 8 线程 CPU

下面每个物理核心有两个硬件线程。操作系统会把大量软件线程调度到这些硬件线程上运行。

核心 1
硬件线程 1
硬件线程 2
核心 2
硬件线程 3
硬件线程 4
核心 3
硬件线程 5
硬件线程 6
核心 4
硬件线程 7
硬件线程 8
软件线程 A 软件线程 B 软件线程 C 软件线程 D 软件线程 E 软件线程 F 软件线程 G 软件线程 H 软件线程 I 软件线程 J
点击上面的软件线程:它们不是固定绑定某个 CPU 线程,而是由操作系统调度到有限的硬件线程上运行。

8. 今天有代表性的问题

这些问题背后,正好串起了并发、调度、语言实现和底层 IO。

不是。Python 线程仍然是线程,因为它们共享同一个进程的内存和资源。 GIL 限制的是同一进程内多个线程执行 Python 字节码的并行能力,而不是把线程变成进程。 CPU 密集任务推荐多进程;IO 密集任务线程仍然有价值。
思想上相同,都是为了避免一个线程傻等一个 IO;层级不同。 协程是语言层的任务抽象,select/poll/epoll 是操作系统层的 IO 多路复用机制。 协程通常通过事件循环间接使用这些机制。
会。线程可能在任意位置被操作系统抢占;协程通常在 await/yield 处主动让出。 所以协程竞争更可控,但如果在修改共享状态中间 await,也可能出现数据不一致。
CPU 的线程是硬件线程,也叫逻辑处理器。程序里的线程是软件线程。 操作系统负责把软件线程调度到 CPU 的硬件线程上执行。软件线程数量可以远大于硬件线程数量。
当接口内部要 await 异步数据库、异步 HTTP、WebSocket、异步文件或其他 IO 操作时,用 async def。 如果里面是 CPU 密集计算或同步阻塞库,不能直接塞进 async def,否则会阻塞事件循环。

9. 小测验

点一下选项,看看今天的核心概念有没有串起来。

1. 协程最适合哪类任务?

A. 纯 Python 大量 for 循环计算
B. 高并发网络 IO 等待
C. GPU 矩阵计算
正确答案:B。协程适合等待型任务,比如网络、数据库、WebSocket、HTTP 请求。

2. epoll 主要解决什么问题?

A. 自动让 Python 多线程绕过 GIL
B. 让 CPU 核心数量翻倍
C. 一个线程高效监听大量 IO 事件
正确答案:C。epoll 是 Linux 上高效的 IO 多路复用机制。

3. 进程和线程最大的资源区别是什么?

A. 进程默认内存隔离,线程共享进程内存
B. 线程一定比进程更安全
C. 进程不能被操作系统调度
正确答案:A。进程是资源分配单位,线程共享所在进程的资源。

4. Python GIL 主要限制什么?

A. 所有进程不能并行
B. 同一 CPython 进程内多个线程执行 Python 字节码
C. C++ 线程不能并行
正确答案:B。GIL 不会把线程变成进程,也不影响多个 Python 进程各自运行。
学习路线建议:下一步可以继续学 asyncio 任务取消、线程池/进程池、FastAPI 后台任务与任务队列。