run_in_executor 是唯一靠谱解法,因其将同步阻塞函数(如time.sleep、requests.get)移交线程池执行,避免阻塞事件循环;必须传入纯同步函数,禁止在其中调用asyncio.run等异步操作。

直接在 asyncio.run() 或事件循环中调用 time.sleep()、requests.get()、json.load() 这类同步阻塞函数,会卡死整个事件循环——其他所有协程全部停摆,异步退化为串行。
为什么 run_in_executor 是唯一靠谱的解法
Python 的异步生态只对「可挂起」的操作友好:await 后面必须是协程对象或实现了 __await__ 的对象。而 time.sleep()、subprocess.run()、大多数数据库驱动(如 psycopg2.connect())、老版本 SDK 等,内部全是系统调用阻塞,无法被事件循环接管。
此时唯一安全路径是:把同步调用丢进线程池执行,主线程(事件循环)继续调度其他协程。
-
loop.run_in_executor(None, func, *args)用默认线程池(concurrent.futures.ThreadPoolExecutor),适合低频、不可控的调用 - 显式传入
ThreadPoolExecutor(max_workers=4)可控并发数,避免线程爆炸(比如批量处理 1000 个文件时,别让 1000 个线程同时开) - 不能用
ProcessPoolExecutor替代——进程间通信开销大,且多数 I/O 操作不受益于多进程
常见误用:在 executor 里又调用 asyncio.run()
有人写这样的代码:
def sync_wrapper():
return asyncio.run(another_async_func()) # ❌ 错误嵌套
<p>loop.run_in_executor(None, sync_wrapper)
</p>
这会导致每个线程都启动一个全新事件循环,资源泄漏、难以调试、性能崩坏。正确做法是:只在线程里跑纯同步逻辑;异步逻辑仍由主事件循环驱动。
- ✅ 正确:用
run_in_executor包裹requests.get()、open().read()、cv2.imread() - ❌ 错误:在
run_in_executor中再调用asyncio.run()、loop.run_until_complete() - ⚠️ 特殊情况:若必须桥接第三方异步库(如旧版
aiofiles不兼容当前 Python),应单独建一个长期存活的子线程+独立 loop,而非每次新建
性能与线程数设置的关键权衡
线程池不是越多越好。操作系统对线程数量有限制(Linux 默认 RLIMIT_NPROC 通常为 1024),且线程切换本身有成本。I/O 密集型任务一般设为 CPU 核心数 × 2~4 即可;CPU 密集型应避免用线程池,改用 ProcessPoolExecutor 并确保主协程不参与计算。
- 默认
None参数使用concurrent.futures.ThreadPoolExecutor(max_workers=61)(CPython 3.8+),对多数 Web 场景偏高 - 高频小任务(如解析 JSON 字符串)建议
max_workers=8~16,配合连接池复用 - 读大文件或调外部 CLI 工具时,
max_workers应 ≤ 4,否则磁盘/网络带宽成瓶颈 - 记得在应用退出前
executor.shutdown(wait=True),否则可能丢失未完成任务
真正容易被忽略的是错误传播:run_in_executor 抛出的异常不会自动进入 except 块,必须显式 await 后再 try/except,或者用 asyncio.shield() 包裹防止取消干扰。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











