并发编程不是为了炫技,而是为了解决实际性能问题。但很多人一上来就纠结“到底用多线程还是多进程还是协程”,结果选错方案导致性能不升反降。这一节直接按场景分类,告诉你什么时候该用什么,以及怎么用。
先记住一条核心决策法则
- CPU 密集型(大量计算,如矩阵运算、图片处理)→ 多进程
- IO 密集型(网络请求、文件读写、数据库查询)→ 协程 或 线程池
- 混合场景(又有计算又有 IO)→ 多进程 + 协程/线程池组合
Python 里多线程因为 GIL 的存在,在 CPU 密集型任务上几乎是“假并发”,无法利用多核。但线程池在 IO 密集型任务里依然好用,因为线程在等待 IO 时会释放 GIL。
场景一:CPU 密集型 — 多进程
典型需求:对 100 万张图片进行缩放、给大量数据做复杂数学运算、跑多个互相独立的模型推理。
为什么用多进程:每个进程有独立的 Python 解释器和 GIL,可以真正并行利用多核 CPU。
推荐工具:concurrent.futures.ProcessPoolExecutor 或 multiprocessing.Pool
简单示例:
from concurrent.futures import ProcessPoolExecutor
import math
def compute_heavy(n):
return sum(math.sqrt(i) for i in range(n))
if __name__ == '__main__':
tasks = [10_000_000] * 8
with ProcessPoolExecutor() as executor:
results = executor.map(compute_heavy, tasks)
注意点:
- 进程间数据传递需要进行序列化(pickle),如果输入数据很大,序列化开销可能吃掉收益,此时可考虑用文件、内存映射或共享内存传递数据。
- 必须在
if name == 'main':保护下运行,否则会无限创建子进程。
场景二:IO 密集型(大量网络请求)— 协程
典型需求:并发调用上百个 API 接口、爬取数千个网页、同时处理多个 WebSocket 连接。
为什么用协程:单线程内利用事件循环切换任务,开销极小,可以轻松支撑上万个并发连接,且没有线程切换的开销和 GIL 的困扰。
推荐工具:asyncio + aiohttp / httpx(异步 HTTP 客户端)
简单示例:
import asyncio
import httpx
async def fetch(url):
async with httpx.AsyncClient() as client:
resp = await client.get(url)
return resp.status_code
async def main():
urls = [f'https://httpbin.org/delay/1' for _ in range(20)]
tasks = [fetch(url) for url in urls]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
注意点:
- 必须使用支持异步的库,否则异步调用会阻塞事件循环(如用
requests就废了)。 - 协程不能用多核加速计算,如果你在协程里做重计算会阻塞整个事件循环,这种情况下应该把计算任务交给进程池。
场景三:IO 密集型(数据库 / 文件操作)— 线程池
典型需求:并发读写多个本地文件、并发查询数据库(当数据库驱动不支持异步时)。
为什么用线程池:对于阻塞式 IO(如文件读写、传统 MySQL 驱动),线程池可以让多个线程同时等待 IO,虽然线程有额外内存开销,但由于 GIL 在 IO 时释放,实际并发度足够。而且相比协程,线程池对现有同步库的兼容性最好,改造代价低。
推荐工具:concurrent.futures.ThreadPoolExecutor
简单示例:
from concurrent.futures import ThreadPoolExecutor
import time
import threading
def read_file(path):
with open(path, 'r') as f:
return len(f.read())
files = ['data1.txt', 'data2.txt', ...]
with ThreadPoolExecutor(max_workers=10) as executor:
results = executor.map(read_file, files)
注意点:
- 线程数不要开得过多,一般几十到几百足够,否则上下文切换开销反而拖慢速度。
- 如果后期数据库驱动升级支持异步,可以无痛切换到协程方案。
场景四:混合任务 — 多进程 + 协程/线程池
典型需求:一批任务中既有图像处理(CPU 密集)又有文件写入(IO 密集),或者每个进程内部还需要并发发送 HTTP 请求。
典型做法:
- 外层用多进程切分计算任务,每个进程内部再用协程或线程池处理 IO 部分。
- 例如,用
ProcessPoolExecutor启动多个进程,每个进程里跑一个asyncio.run()处理自身的一批网络请求。
简单示例:
import asyncio
import httpx
from concurrent.futures import ProcessPoolExecutor
async def worker(urls):
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
return await asyncio.gather(*tasks)
def run_worker(urls):
return asyncio.run(worker(urls))
def main():
all_urls = [f'https://httpbin.org/delay/1' for _ in range(100)]
# 将 URL 列表分成几块,每个进程处理一块
chunk_size = 20
chunks = [all_urls[i:i+chunk_size] for i in range(0, len(all_urls), chunk_size)]
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(run_worker, chunks)
小结:选择一张图搞定
| 任务类型 | 推荐方案 | 常用工具 |
|----------------|--------------|-------------------------------|
| CPU 密集 | 多进程 | ProcessPoolExecutor |
| 网络 IO 密集 | 协程 | asyncio + aiohttp/httpx |
| 文件/传统 DB IO | 线程池 | ThreadPoolExecutor |
| 混合计算 + IO | 多进程 + 协程 | ProcessPoolExecutor 内嵌 asyncio |
上面的方案足以应对 90% 的 Python 并发加速需求。关键是不盲目跟风,先判断任务的瓶颈在 CPU 还是 IO,然后选用对应的并发模型,才能用最小的改动换回最大的性能提升。