在学完多线程、多进程和异步 IO 之后,一个自然的问题就是:我的任务到底该用哪种并发方案? 答案取决于你的任务是 CPU 密集型 还是 IO 密集型。选对了方案,性能可能翻几倍;选错了,可能比单线程还慢。
CPU 密集型任务
典型场景:大规模数值计算、图像处理、视频编解码、模型训练、加解密运算——核心瓶颈在于 CPU 一直处于高负载运算状态。
最优解:多进程(multiprocessing)
- 原因:GIL(全局解释器锁)导致 Python 多线程在同一时刻只能有一个线程执行字节码。对于 CPU 密集型任务,多线程不仅无法利用多核,还会因线程切换带来额外开销。
- 多进程的优势:每个进程拥有独立的 GIL 和内存空间,可以真正并行地利用多个 CPU 核心。
- 推荐用法:
- 使用
multiprocessing.Pool创建进程池,pool.map()自动分配任务。 - 或者用更简洁的
concurrent.futures.ProcessPoolExecutor。 - 代码示例:
from multiprocessing import Pool
def heavy_compute(n):
return sum(i * i for i in range(n))
if __name__ == '__main__':
with Pool() as pool:
results = pool.map(heavy_compute, [10**7, 10**7, 10**7, 10**7])
备选方案:如果不想引入多进程的复杂性,对于部分数值计算可以考虑:
- C 扩展 / Cython:把计算密集部分提取到 C 代码中,绕过 GIL。
- Numba:使用
@jit装饰器加速 Python 数值计算。 - PyPy:对于纯 Python 算法代码,PyPy 的 JIT 可能带来 3~5 倍性能提升。
IO 密集型任务
典型场景:网络请求(爬虫、API 调用)、文件读写、数据库操作、用户输入等待——核心瓶颈在于大部分时间花在等待 IO 响应,CPU 经常空闲。
最优解:异步 IO(asyncio)或 多线程
- 多线程:GIL 在 IO 操作时会主动释放,所以多个线程可以并发地等待不同的 IO,从而极大减少总体等待时间。编写起来比较直观,尤其适合 IO 操作时间较长或数量不特别庞大的场景。
- 异步 IO(asyncio):基于协程和事件循环,单线程就可以管理成千上万个并发连接。因为切换开销极小,在需要处理大量并发连接(比如 10000 个 API 请求)时,性能远超多线程。但需要配合支持异步的库(如
aiohttp、asyncpg),代码风格也与同步代码有差异。 - 线程池/进程池:
concurrent.futures的线程池也是处理 IO 密集任务的好用工具,提供了统一的submit和map接口。
实际选择经验:
- 如果并发量在几十到几百,用 多线程 或 线程池 最省心,代码改动小。
- 如果并发量上千甚至上万,用 asyncio 是最佳选择,否则大量线程的内存和切换开销会拖垮系统。
- 文件 IO 注意:操作系统对文件的异步支持有限(尤其是 Linux 的普通文件读写),此时用线程池通常比 asyncio 更方便。
代码对比:
# 多线程示例
from concurrent.futures import ThreadPoolExecutor
import requests
urls = ["https://httpbin.org/delay/1"] * 10
def fetch(url):
return requests.get(url).status_code
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(fetch, urls))
# 异步 IO 示例
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as resp:
return resp.status
async def main():
urls = ["https://httpbin.org/delay/1"] * 10
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
asyncio.run(main())
混合场景:既有 CPU 又有 IO
现实中不少任务并非纯粹一种。例如:
- 爬虫:网络 IO 密集(下载页面),但解析页面可能包含一些 CPU 操作(正则、BeautifulSoup)。通常选择 asyncio + 多进程池或 asyncio + 线程池的组合。
- Web 服务:请求处理过程中,如果涉及长时间 CPU 计算,可以在异步视图里使用
loop.run_in_executor()将计算委托到线程池或进程池,避免阻塞事件循环。
决策速查表
| 任务类型 | 推荐方案 | 运行效果 |
|---------|---------|---------|
| 纯 CPU 密集计算 | multiprocessing / ProcessPoolExecutor | 真正多核并行 |
| IO 密集,并发量数百以下 | ThreadPoolExecutor / threading | 代码改动小,效果明显 |
| IO 密集,并发量成千上万 | asyncio + 异步库 | 单线程极高并发 |
| 既有大量 IO 又有较重计算 | asyncio + run_in_executor 混合 | 兼顾并发与性能 |
记住这个核心判断:先问自己“程序的大部分时间在等什么”——在等网络/磁盘就用多线程或异步;在等 CPU 算完就用多进程。这几乎是 Python 并发方案选择的不二法则。