Python 并发模型选型指南

8次阅读

asyncio适合I/O密集、无强状态依赖且全异步支持的场景;不适合CPU密集、含阻塞C扩展或依赖同步库的情况,混用会阻塞事件循环

Python 并发模型选型指南

asyncio 适合什么场景,又不适合什么

如果 I/O 密集、协程间无强状态依赖、且你能控制所有关键库是否支持异步(比如 aiohttp 而非 requests),asyncio 是首选。它在单线程内靠事件循环调度,开销小、并发高。

但一旦混入 CPU 密集操作(如图像处理、数值计算),或调用大量阻塞式 C 扩展(如某些 numpy 操作未显式释放 GIL)、或依赖的第三方库只有同步接口(如多数 psycopg2 旧版本),asyncio 就会卡住整个事件循环——这不是 bug,是设计使然。

  • 别指望 asyncio.to_Thread() 能“拯救”所有同步调用;频繁跨线程切走会抵消异步收益
  • asyncio.run() 每次都新建事件循环,短生命周期脚本没问题,长期服务要自己管理 asyncio.get_Event_loop()
  • 调试时看到 RuntimeWarning: coroutine 'xxx' was never awaited,基本是忘了 await 或误把协程当普通函数调了

多进程 vs 多线程:GIL 不是唯一决定因素

python 的 GIL 确实让多线程无法真正并行执行 CPU 密集任务,但这不等于“线程没用”。对 I/O 密集型任务(如大量 HTTP 请求、文件读写),threading 开销低、启动快、共享内存方便,反而是更轻量的选择。

multiprocessing 虽能绕过 GIL,但进程创建/通信成本高,对象序列化(pickle)限制多,且 windows 下默认启动方式是 spawn,导致模块级代码重复执行——常见错误是把 if __name__ == '__main__': 漏掉,直接报 AttributeError: Can't get attribute 'xxx' on

立即学习Python免费学习笔记(深入)”;

  • concurrent.futures.ThreadPoolExecutor 控制线程数比裸写 threading.Thread 更安全,避免资源耗尽
  • multiprocessing.Poolmap() 默认不支持关键字参数;要用 starmap()封装成单参数元组
  • 子进程无法继承父进程的 Logging 配置,需显式重置 handler 和 level

何时该上 trio / curio,而不是死磕 asyncio

asyncio 的 API 设计偏底层,取消传播、超时嵌套、任务隔离等逻辑容易出错。如果你的项目需要强可靠性(比如金融类定时结算、设备长连接保活),trio 的结构化并发模型会更省心:每个 async with trio.open_nursery() 块天然保证子任务全部结束才退出,异常自动传播,无需手动 gather() + cancel()

但代价是生态窄——trio 兼容的 HTTP 客户端只有 httpx(且得指定 backend="trio"),数据库驱动几乎为零。你不能一边用 trio 写主逻辑,一边拿 asyncpg(只支持 asyncio)去查库,否则会触发 RuntimeError: this event loop is already running

  • curio 更激进,连 async/await 都不用,全靠 async def + await + 显式 sleep(),学习曲线陡,社区支持弱,仅建议实验性项目尝试
  • 别试图用 anyio 当万能胶水——它只是统一了 API 表面,底层仍是 asynciotrio,切换后仍要重测所有 I/O 组件

混合模型不是银弹,但有时真得这么干

现实系统常同时存在 CPU 密集(如视频转码)、I/O 密集(如上报日志)、和外部阻塞调用(如调用某老 C 库的 SDK)。纯一种模型撑不住,必须分层:主线程asyncio 处理网络,CPU 工作扔给 multiprocessing.Process,阻塞 SDK 调用丢进 concurrent.futures.ThreadPoolExecutor 并设好 max_workers=1 防止线程爆炸。

难点不在启动,而在状态同步与错误透传。比如子进程崩溃,asyncio 主循环不会自动感知;线程池里抛了异常,若没用 future.result() 主动取,就会静默丢失。

  • asyncio.Queue 在协程与子进程间传数据,别用 multiprocessing.Queue——后者不 await 友好
  • 所有跨模型边界的操作,务必加超时(asyncio.wait_for()future.result(timeout=...)),否则一个卡死就拖垮全局
  • logging 本身是线程安全的,但多进程下需用 QueueHandler + 单独日志进程,否则日志错乱或丢失
text=ZqhQzanResources