协程与线程与进程选型决策树
一、协程 vs 线程 vs 进程——选型决策树
1.1 核心区别——一张表
| 协程(Coroutine) | 线程(Thread) | 进程(Process) | |
|---|---|---|---|
| 调度者 | 程序自己(用户态) | 操作系统(内核态) | 操作系统(内核态) |
| 切换开销 | ~纳秒级(函数调用级别) | ~微秒级(系统调用 + 上下文切换) | ~毫秒级(地址空间切换) |
| 内存 | 共享——同一个线程 | 共享——同一个进程 | 隔离——各自独立内存空间 |
| GIL 影响 | 不受 GIL 限制 | 受 GIL 限制——同时只能一个线程跑 Python | 不受 GIL——各自有 Python 解释器 |
| 通信方式 | 直接返回值 / 共享变量 | Queue / Lock / 共享变量(需加锁) | Pipe / Queue / 共享内存 |
| 创建数量 | 成千上万无压力 | 几十到几百(多了切换开销大) | 几到几十(启动慢、内存重) |
| 适用场景 | I/O 密集型(网络、DB) | I/O 密集型(需调用不兼容 asyncio 的库) | CPU 密集型(计算、图像处理) |
| 崩溃影响 | 一个协程崩溃可能影响整个事件循环 | 一个线程崩溃可能带崩进程 | 一个进程崩溃独立隔离 |
1.2 为什么 Python 多线程”不行”——GIL 一句话
# ❌ 多线程算数学——和单线程一样快(甚至更慢)
import threading
results = []
def compute():
for i in range(10_000_000):
results.append(i * i) # 这行需要 GIL
# ✅ 多线程等 I/O——有用(等 I/O 时会释放 GIL)
def download():
resp = requests.get(url) # 底层 C 代码等网络时会释放 GIL
# 另一个线程可以趁这个空隙执行
一句话讲清:“GIL 导致 Python 多线程无法利用多核做计算。但 I/O 密集场景下,线程等在系统调用时会释放 GIL——还是有用的。asyncio 更彻底——单线程跑协程,根本就不碰 GIL。“
1.3 选型决策树
graph TD TASK[你要执行什么任务] --> IO[IO密集型<br/>大量网络/数据库/文件操作] TASK --> CPU[CPU密集型<br/>大量计算] IO --> ASYNC[库支持asyncio → asyncio 首选] IO --> THREAD[库不支持asyncio → 多线程 run_in_executor] CPU --> PROCESS[多进程 ProcessPoolExecutor 绕开GIL]
# 实战:asyncio + 线程池混用——搞定不兼容 asyncio 的库
import asyncio
import requests # 同步库,但不支持 asyncio
async def download_with_requests(url):
loop = asyncio.get_running_loop()
# 把同步函数丢到线程池执行——不阻塞事件循环
return await loop.run_in_executor(
None, # None = 默认线程池
requests.get, # 同步函数
url # 参数
)
# 现在可以"像异步一样"并发调用同步 requests 了
async def main():
results = await asyncio.gather(
download_with_requests("url1"),
download_with_requests("url2"),
download_with_requests("url3"),
)
速记卡(面试闪卡)
Q1:一句话讲清「协程与线程与进程选型决策树」到底是什么?
A:协程、线程、进程是按任务类型选型的三种并发模型:协程管 I/O、进程管 CPU、线程居中。
Q2:核心区别一张表 —— 怎么理解?
A:三者像三种搬砖工:协程(coroutine)是同一个人在任务间秒切(纳秒级、共享内存、不受 GIL);线程(thread)是工厂多个工位的工人(微秒级、受 GIL 同时只一人跑 Python);进程(process)是独立工厂(毫秒级、各占内存、真并行)。
Q3:GIL 一句话 —— 怎么理解?
A:GIL(Global Interpreter Lock)像 Python 工厂的唯一厕所钥匙:同一刻只有一个线程能跑 Python 字节码,多线程算数学和单线程一样快。但等 I/O(网络/磁盘)时会松手,所以 I/O 密集仍有用;asyncio 更狠——单线程跑协程根本不碰这把锁。
Q4:选型决策树 —— 怎么理解?
A:决策像分诊台:I/O 密集→库支持 asyncio 就上协程,不支持就丢线程池 run_in_executor;CPU 密集→多进程 ProcessPoolExecutor 绕开 GIL。一句话:算多用进程,等多用协程/线程。
Q5:asyncio + 线程池混用 —— 怎么理解?
A:遇到不兼容 asyncio 的同步库(如 requests),别在协程里直接调——会堵死事件循环。用 loop.run_in_executor(None, requests.get, url) 把它丢到默认线程池跑,等 I/O 时协程切走,像把脏活外包给临时工不误正事。
Q6:核心速记主线有哪些?
-
协程:用户态、纳秒切换、不受 GIL、I/O 密集、数量成千上万
-
线程:内核态、受 GIL(同时一线程跑 Python)、I/O 密集、几十到几百
-
进程:独立内存、绕开 GIL、CPU 密集、几到几十、崩溃隔离
-
GIL 使多线程不能并行计算,但 I/O 时释放 GIL 仍有用
-
选型:CPU 密集→多进程;I/O 且支持 asyncio→协程;不支持→线程池
口诀
A:协线程进程三兄弟,调度开销各不同;
GIL 锁住多线程,算数枉费工;
IO 密集用协程,CPU 密集开进程;
不兼容库丢线程池,事件循环莫堵。
相关链接
-
📋 目录:00-Python
-
📚 学习清单:八股文学习路线图
-
🔗 IO密集并发选型
-
🔗 进程vs线程vs协程 — 操作系统视角的并发基础
-
🔗 JS异步与事件循环 — Python asyncio vs JS 事件循环
-
🔗 进程vs线程vs协程 — 操作系统视角的三者对比