run_in_executor不能直接传async函数,因其设计目标是执行同步阻塞操作,而async函数返回协程对象,不可哈希且无法在线程/进程中直接调用。

run_in_executor 为什么不能直接传 async 函数?
run_in_executor 的设计目标是把同步阻塞调用(比如文件读写、正则匹配、加密解密)扔进线程池或进程池执行,避免阻塞事件循环。它**不接受 async def 定义的协程对象**——传进去会立刻报 TypeError: unhashable type: 'coroutine' 或直接静默失败。
常见错误是这样写:
async def slow_db_query():
return await db.execute("SELECT ...") # 这是 async 函数
<h1>❌ 错误:传协程对象,不是可调用对象</h1><p>await loop.run_in_executor(None, slow_db_query())正确做法是传函数名 + 参数,让 executor 在子线程里调用同步版本:
立即学习“Python免费学习笔记(深入)”;
- 确保你要跑的是纯同步函数(如
requests.get、json.loads、subprocess.run) - 如果只有 async 版本(如
aiohttp.ClientSession),别用run_in_executor,直接 await 即可 - 需要包装时,用普通
def包一层,内部调用同步逻辑
线程池 vs 进程池:选哪个 executor?
默认 run_in_executor(None, ...) 用的是 ThreadPoolExecutor,适用于 I/O 密集型阻塞操作(如 HTTP 请求、磁盘读取)。但如果你的阻塞操作是 CPU 密集型(比如 numpy.linalg.svd、xml.etree.ElementTree.parse 大文件),线程池没用——CPython 的 GIL 会让它们串行执行。
这时该换 ProcessPoolExecutor:
from concurrent.futures import ProcessPoolExecutor import asyncio <p>loop = asyncio.get_running_loop() with ProcessPoolExecutor() as pool: result = await loop.run_in_executor(pool, cpu_heavy_task, data)
注意:ProcessPoolExecutor 要求目标函数和参数都可序列化(pickleable),闭包、lambda、类实例方法通常不行;且进程启动开销大,不适合高频小任务。
- I/O 阻塞 → 默认
None(线程池) - CPU 阻塞 → 显式传
ProcessPoolExecutor - 长期运行或需共享状态 → 考虑用
asyncio.to_thread(Python 3.9+)替代手动管理线程池
参数传递和异常传播怎么不出错?
run_in_executor 本身不会吞异常,但它在线程/进程中抛出的异常,会在 await 处以原样重新抛出。问题常出在参数上:
- 传入不可序列化的对象(如打开的文件句柄、数据库连接、
asyncio.Lock)→ 进程池直接崩溃,线程池可能报PicklingError - 传入 lambda 或嵌套函数 → 线程池能跑,进程池会失败
- 传入带装饰器的函数 → 某些装饰器(如
@functools.lru_cache)可能在线程间共享状态,引发竞态
安全做法:
def safe_parse_xml(raw_bytes):
# 所有输入都是基础类型或 bytes,不依赖外部状态
import xml.etree.ElementTree as ET
return ET.fromstring(raw_bytes)
<h1>✅ 安全:bytes 可序列化,函数干净</h1><p>result = await loop.run_in_executor(None, safe_parse_xml, b"<root><item>1</item></root>")如何避免 event loop 关闭时 run_in_executor 挂住?
当程序退出、服务重启或 asyncio.run() 结束时,如果还有 run_in_executor 任务在跑,主线程可能卡住等它们完成——尤其进程池里的子进程不会自动被杀。
关键点:
- 不要复用全局
ProcessPoolExecutor实例而不显式.shutdown() - 在
finally或asyncio.CancelledError捕获块中,调用executor.shutdown(wait=False) - 对长时间运行的任务,考虑加超时:用
asyncio.wait_for(..., timeout=5.0)包裹 await 表达式 - Python 3.9+ 推荐用
asyncio.to_thread(),它内部自动处理线程生命周期,更轻量
真正容易被忽略的是:哪怕你用了 asyncio.run(),如果 run_in_executor 里调用了阻塞时间极长的 C 扩展(比如某些 SSL 握手失败场景),它仍可能绕过 Python 层的取消机制,导致无法中断。


















