一句话总结
Python 并发的真相是"分工":CPU 密集用多进程绕开 GIL,I/O 密集用 asyncio 协程以单线程支撑海量并发,线程仅适合少量阻塞 I/O。进阶能力 = 把事件循环玩透 + 用对武器 + 用数据驱动性能优化。
| 场景 | 首选 | 原因 |
|---|---|---|
| CPU 密集(计算/压缩) | multiprocessing | 绕过 GIL 真并行 |
| I/O 密集(网络/DB) | asyncio | 协程低开销高并发 |
| 少量阻塞调用 | threading | 简单,受 GIL 限制 |
asyncio 事件循环深入
事件循环(Event Loop)是 asyncio 的心脏:它维护一个就绪队列,不断地"取一个协程跑一会、遇 await 就挂起让出、等 I/O 就绪再恢复"。单线程内通过协作式多任务实现并发。
async/await标记协程;await处让出控制权,不阻塞线程。asyncio.create_task()把协程调度到循环并发执行;gather并发收集结果。- 陷阱:在协程里调用同步阻塞函数(如
time.sleep、未异步化的 DB 驱动)会卡住整个循环。
import asyncio
async def fetch(name, delay):
await asyncio.sleep(delay) # 非阻塞让出
return f"{name} done"
async def main():
# 并发执行而非串行
results = await asyncio.gather(
fetch("A", 1), fetch("B", 2), fetch("C", 1)
)
print(results)
asyncio.run(main())
多进程 / 线程 / 协程选型
GIL(全局解释器锁):同一时刻只有一个线程执行 Python 字节码,所以多线程无法在 CPU 密集任务上真并行。
- 多进程:每进程独立 GIL 与内存,适合 CPU 密集;代价是进程间通信(Queue/Manager)开销大。
- 线程:受 GIL 限制,仅适合 I/O 等待期间并发;适合包裹少量阻塞库。
- 协程:用户态调度,单线程数万并发,内存占用极低;是后端高并发 I/O 的首选。
实践中常组合:多进程 + 每进程一个事件循环(如 gunicorn + uvicorn worker),横向用进程、纵向用协程,最大化多核与并发。
性能工程
性能优化必须先度量后优化。用 cProfile 定位热点,避免凭直觉:
import cProfile
cProfile.run("heavy_compute()", sort="cumulative")
- 异步框架:FastAPI/Starlette 基于 asyncio,单实例即可扛高并发请求;配合
async def路由与异步 DB 驱动(asyncpg/aiomysql)。 - 数据库:用连接池(SQLAlchemy
asyncioengine)避免频繁建连;批量写代替循环单条。 - 序列化:orjson 替代 json 提速明显;热点路径避免不必要对象分配。
- C 扩展:超热点可用 Cython 或调用 numpy 向量化,进一步榨干 CPU。
工程化
大型 Python 后端必须重视工程化,否则类型与依赖失控:
- 类型标注 + mypy:全量标注函数签名,CI 里跑
mypy --strict提前抓类型错误,是降低线上事故最有效的手段之一。 - 虚拟环境与锁定:
venv+pip-tools/poetry 锁定依赖,保证环境可复现。 - 异步 ORM:用 SQLAlchemy 2.0 async 或 Tortoise-ORM,路由内全程 async。
- 结构化日志:用 structlog/loguru 输出 JSON 日志,便于采集与链路关联。
面试模拟
Q1:asyncio 里调用 time.sleep 会怎样?
答:time.sleep 是同步阻塞,会卡住整个事件循环,所有协程都停摆;应改用 await asyncio.sleep。
Q2:为什么 CPU 密集任务用多线程没用?
答:GIL 让同一时刻仅一个线程执行字节码,多线程在 CPU 密集下是交替而非并行,甚至因切换更慢;应用多进程。
Q3:FastAPI 为什么比传统 Flask 并发高?
答:FastAPI 基于 asyncio,每个请求在 I/O 等待时让出循环处理其他请求;Flask 默认同步阻塞,并发靠线程/进程数,开销大。
Q4:协程比线程轻量在哪?
答:协程是用户态调度,单协程栈仅 KB 级,可轻松开数万;线程由内核调度,默认栈 MB 级,上下文切换成本高。