Python 进程、线程、协程到底是什么?一篇讲清楚适用场景和区别
Python 进程、线程、协程到底是什么?一篇讲清楚适用场景和区别
学习 Python 并发时,最容易混在一起的三个概念就是:进程、线程、协程。
很多资料会直接给结论:
- CPU 密集型用多进程
- IO 密集型用多线程或协程
- 大量异步 IO 用协程
这些结论没错,但如果只背结论,面试时很容易被追问卡住。本文用比较直观的方式,把进程、线程、协程的本质、区别、适用场景和 Python 里的典型写法讲清楚。
一、先给结论
| 类型 | 谁调度 | 内存关系 | 创建和切换成本 | 适合场景 |
|---|---|---|---|---|
| 进程 | 操作系统 | 进程间内存隔离 | 高 | CPU 密集、多核并行 |
| 线程 | 操作系统 | 同一进程内线程共享内存 | 中 | 阻塞 IO、少量到中等并发 |
| 协程 | 程序事件循环 | 通常在同一线程内协作执行 | 低 | 大量非阻塞 IO |
一句话总结:
进程是资源分配单位,线程是操作系统调度的执行单位,协程是用户态由事件循环调度的轻量任务。
二、什么是进程?
进程是操作系统分配资源的基本单位。
当我们运行一个 Python 脚本:
python sync_baseline.py
操作系统会启动一个 Python 进程。这个进程拥有自己的内存空间、变量、文件句柄和运行环境。
进程之间默认是隔离的。一个进程里的变量,另一个进程不能直接访问。
进程的特点
- 进程之间内存隔离,稳定性更好。
- 一个进程崩溃,通常不会直接破坏另一个进程的内存。
- 进程创建、销毁和切换成本比较高。
- 多进程可以真正利用多个 CPU 核心。
- 进程之间通信需要额外机制,例如 Queue、Pipe、Socket、共享内存等。
进程适合什么场景?
进程适合 CPU 密集型任务。
比如:
- 图片处理
- 视频转码
- 大批量数据计算
- 压缩和解压
- 复杂文本解析
- 机器学习中的特征处理
这些任务的特点是:CPU 一直在忙着算。
Python 中由于 GIL 的存在,纯 Python 多线程很难让多个 CPU 核心同时执行 Python 字节码。而多进程是多个独立的 Python 解释器进程,可以绕开 GIL 的限制。
多进程示例
from concurrent.futures import ProcessPoolExecutor
def cpu_task(n: int) -> int:
return sum(i * i for i in range(n))
def main() -> None:
numbers = [1_000_000, 1_000_000, 1_000_000, 1_000_000]
with ProcessPoolExecutor() as pool:
results = list(pool.map(cpu_task, numbers))
print(results)
if __name__ == "__main__":
main()
这个例子中,每个 CPU 计算任务可以分配给不同进程执行,从而利用多核。
三、什么是线程?
线程是进程里的执行单位。
一个进程里可以有多个线程。它们共享同一个进程的内存空间。
可以这样理解:
- 进程像一个公司。
- 线程像公司里的员工。
- 公司有自己的办公室、电脑、资料柜。
- 员工共享这些资源,但每个人可以处理不同任务。
线程的特点
- 多个线程共享同一个进程的内存。
- 创建和切换成本低于进程。
- 线程由操作系统调度。
- 线程之间共享变量方便,但也容易出现线程安全问题。
- Python 纯 CPU 密集型多线程会受到 GIL 限制。
- 对阻塞 IO 任务,多线程仍然有明显价值。
线程适合什么场景?
线程适合阻塞 IO。
比如:
- 使用
requests请求多个 URL - 读写多个文件
- 等待数据库查询返回
- 调用第三方接口
- 少量到中等数量的并发任务
这些任务的特点是:程序大部分时间不是在计算,而是在等待外部系统返回。
多线程示例
from concurrent.futures import ThreadPoolExecutor
import time
def fetch(index: int) -> str:
time.sleep(0.2)
return f"ok-{index}"
def main() -> None:
with ThreadPoolExecutor(max_workers=5) as pool:
results = list(pool.map(fetch, range(10)))
print(results)
if __name__ == "__main__":
main()
这里的 time.sleep(0.2) 模拟一次阻塞 IO。
如果同步执行 10 次,大约需要 2 秒。如果开 5 个线程,多个任务可以同时等待,总耗时会明显降低。
为什么说线程适合 IO?
因为 IO 等待期间,线程并没有一直占用 CPU。
比如线程 A 正在等待网络返回,这时操作系统可以切换到线程 B 执行。这样多个任务的等待时间可以重叠。
所以,线程不是没用。GIL 主要限制的是 CPU 密集型 Python 字节码并行,不代表线程不能提升 IO 密集任务的吞吐。
四、什么是协程?
协程是用户态的轻量级任务,常见于 Python 的 asyncio。
线程由操作系统调度,协程由程序里的事件循环调度。
协程不会自动并行。它是在遇到 await 时主动让出控制权,让事件循环去执行其他已经就绪的协程。
协程的特点
- 非常轻量,可以创建大量协程。
- 切换成本低,因为不需要操作系统线程切换。
- 适合大量非阻塞 IO。
- 需要
async/await调用链支持。 - 如果在协程里直接调用阻塞函数,会卡住整个事件循环。
协程适合什么场景?
协程适合大量非阻塞 IO。
比如:
- 异步 HTTP 请求
- WebSocket 长连接
- 异步数据库访问
- 异步爬虫
- FastAPI 异步接口中的外部 IO
协程的前提是:你使用的库本身要支持异步。
例如:
httpx.AsyncClientaiohttpasyncpgaiomysql
如果你在协程里直接调用 requests.get() 这种阻塞函数,事件循环仍然会被卡住。
协程示例
import asyncio
async def fetch(index: int) -> str:
await asyncio.sleep(0.2)
return f"ok-{index}"
async def main() -> None:
tasks = [fetch(i) for i in range(10)]
results = await asyncio.gather(*tasks)
print(results)
if __name__ == "__main__":
asyncio.run(main())
这里的 await asyncio.sleep(0.2) 不是阻塞当前线程,而是告诉事件循环:
我现在要等 0.2 秒,你先去执行别的协程。
所以 10 个协程可以几乎同时开始等待,总耗时接近 0.2 秒,而不是 2 秒。
五、用同步 IO baseline 理解三者差异
假设有 10 个任务,每个任务都要等待 0.2 秒。
同步代码大概是这样:
import time
def fetch_sync(index: int) -> str:
time.sleep(0.2)
return f"ok-{index}"
def main() -> None:
results = [fetch_sync(i) for i in range(10)]
print(results)
if __name__ == "__main__":
main()
执行顺序是:
任务 1 等 0.2 秒
任务 2 等 0.2 秒
任务 3 等 0.2 秒
...
任务 10 等 0.2 秒
所以总耗时接近:
10 * 0.2 = 2 秒
如果换成线程:
线程 A 在等 IO
线程 B 也在等 IO
线程 C 也在等 IO
多个等待可以重叠,总耗时会下降。
如果换成协程:
协程 A await IO
协程 B await IO
协程 C await IO
协程在 await 时把控制权交回事件循环,事件循环继续推进其他协程,总耗时也会下降。
六、GIL 对线程有什么影响?
GIL 是 Global Interpreter Lock,全局解释器锁。
在 CPython 中,同一时刻通常只有一个线程能执行 Python 字节码。
这意味着:
- 多线程不适合纯 Python CPU 密集计算。
- 多线程仍然适合 IO 密集任务。
为什么?
因为 IO 等待时,线程会释放 CPU,操作系统可以调度其他线程。对于网络请求、数据库等待、文件读写等场景,线程依然能提高吞吐。
所以不能简单地说:
Python 有 GIL,所以线程没用。
更准确的说法是:
GIL 主要限制 CPU 密集型 Python 多线程并行;对于 IO 密集型任务,多线程仍然有实际价值。
七、协程是不是一定比线程快?
不是。
协程快不快,取决于调用链是否真正非阻塞。
如果代码是这样:
async def fetch():
requests.get("https://example.com")
虽然函数写成了 async def,但里面调用的是阻塞 IO。它会卡住整个事件循环。
正确的异步调用应该使用支持 async 的库,例如:
import httpx
async def fetch():
async with httpx.AsyncClient() as client:
response = await client.get("https://example.com")
return response.text
所以,async def 不等于自动更快。真正关键的是:
- 是否存在
await await的对象是否是非阻塞 IO- 调用链是否全程支持异步
八、怎么做技术选型?
可以按下面这个顺序判断。
1. 任务是不是 CPU 密集?
如果 CPU 一直在忙着计算,例如图片处理、复杂解析、大批量计算,优先考虑:
多进程
任务队列
分布式任务
2. 任务是不是 IO 密集?
如果任务主要在等待网络、数据库、磁盘或第三方 API,考虑:
线程
协程
3. 依赖库是否支持 async/await?
如果支持,例如 aiohttp、httpx.AsyncClient、asyncpg,可以优先考虑协程。
如果不支持,例如只能使用 requests 或某些阻塞 SDK,可以考虑线程池。
4. 并发规模有多大?
少量到中等并发,线程池通常更直接。
大量连接、大量等待、大量非阻塞 IO,协程通常更合适。
九、常见误区
误区 1:有 GIL,所以 Python 线程没用
不准确。
GIL 主要影响 CPU 密集任务。IO 密集场景下,多线程仍然能提高吞吐。
误区 2:用了 async def 就一定更快
不准确。
只有调用链里的 IO 操作能 await,协程才有明显收益。
误区 3:进程一定最快
不准确。
进程有创建、序列化、通信成本。大量小任务如果用多进程,可能成本比收益还高。
误区 4:协程能替代所有线程
不准确。
如果依赖库是阻塞的,强行套 async 反而会阻塞事件循环。此时线程池可能更合适。
十、面试怎么回答?
可以这样回答:
进程是操作系统分配资源的基本单位,进程之间内存隔离,适合 CPU 密集型任务,也可以利用多核 CPU。线程是进程内的执行单位,多个线程共享进程内存,适合阻塞 IO,但纯 Python CPU 密集任务会受到 GIL 限制。协程是用户态的轻量级任务,由事件循环调度,适合大量非阻塞 IO,但前提是调用链支持 async/await。实际选型时,我会先判断任务是 CPU 密集还是 IO 密集,再看依赖库是否支持异步,如果是 CPU 密集就考虑多进程,如果是阻塞 IO 可以用线程池,如果是大量非阻塞 IO 则优先用 asyncio、httpx 或 aiohttp。
十一、总结
最后再用一张表收尾:
| 问题 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度者 | 操作系统 | 操作系统 | 事件循环 |
| 是否共享内存 | 默认不共享 | 共享进程内存 | 通常同线程内共享 |
| 成本 | 高 | 中 | 低 |
| 是否适合 CPU 密集 | 适合 | 不适合纯 Python CPU 密集 | 不适合 |
| 是否适合 IO 密集 | 可以,但成本偏高 | 适合阻塞 IO | 适合非阻塞 IO |
| 典型工具 | ProcessPoolExecutor | ThreadPoolExecutor | asyncio / httpx / aiohttp |
最重要的不是记住“哪个更高级”,而是能根据任务瓶颈做判断:
CPU 在忙着算:优先多进程
主要在等外部系统:线程或协程
依赖库支持 async/await:优先协程
依赖库是阻塞调用:线程池更直接
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)