asyncio.run()不能在已有事件循环中调用,因其专为顶层脚本设计,会新建并关闭完整事件循环;在Jupyter、FastAPI等环境中应改用await或asyncio.create_task()。

asyncio.run() 不能直接套用在特征拉取循环里
Python 3.12 的 asyncio.run() 是单次入口,启动一个事件循环后就关闭——如果你在训练 pipeline 中反复调用它拉特征,会报 RuntimeError: asyncio.run() cannot be called from a running event loop。这不是 bug,是设计使然。
实操建议:
- 整个特征拉取阶段(比如从多个 HTTP 接口或 Redis 批量读)必须包在一个顶层
async def main()里,只调一次asyncio.run(main()) - 别在
for循环里写asyncio.run(fetch_feature(...)),改用await asyncio.gather(*coros)并发调度 - 若需分批拉取(如每次 50 个 ID),用
asyncio.create_task()拆成子任务,避免单次gather参数过多导致内存暴涨
HTTP 特征拉取必须换掉 requests,改用 httpx.AsyncClient
requests 是同步阻塞的,放进 async 函数里会卡死整个事件循环;Python 3.12 下 aiohttp 虽可用,但 httpx 对 HTTP/2、连接复用和类型提示支持更稳,且 API 更接近 requests,迁移成本低。
常见错误现象:用了 await asyncio.to_thread(requests.get, url) 包装,看似“异步”,实则只是把阻塞操作扔进线程池——并发高时线程数爆炸,CPU 和上下文切换开销反而更大。
立即学习“Python免费学习笔记(深入)”;
正确做法:
- 初始化一个全局
httpx.AsyncClient实例(带limits和timeout),复用连接池 - 每个请求用
await client.get(url),不是client.get(漏掉await会返回 coroutine 对象,不执行) - 注意
httpx默认不校验证书,生产环境要显式传verify=True,否则可能静默失败
Redis 特征拉取要用 redis-py 4.10+ 的 async 支持
旧版 redis-py(await asyncio.to_thread(r.get, key) 同样有线程池瓶颈;Python 3.12 下推荐直接升级到 redis>=4.10.0,它内置了 redis.asyncio.Redis。
使用场景差异:
- 单 key 查:用
await redis_client.get(key) - 多 key 批量查(如用户画像 10 个字段):优先用
await redis_client.mget(keys),比 10 个await get()快 3–5 倍 - 如果特征存在嵌套结构(如 JSON 字符串),别在 Python 里
json.loads(),改用 Redis 的JSON.GET(需 Redis 7.0+ +redis-py启用decode_responses=False)
并发数控制不当会导致下游服务熔断或超时
异步不是无代价的并发。Python 3.12 的事件循环本身不限制并发量,但目标服务(API 网关、Redis、数据库)通常有连接数、QPS 或响应时间阈值。盲目设 asyncio.gather(*coros) 可能瞬间打出上千请求,触发限流或 503。
实操关键点:
- 用
asyncio.Semaphore(n)控制最大并发数,n不是拍脑袋定的——先压测目标接口,找到稳定响应的 QPS 上限,再除以平均延迟(单位秒)估算合理并发数 - 对不同下游服务用独立 semaphore:HTTP 接口一个,Redis 一个,避免 Redis 慢拖垮 HTTP 请求
- 加 timeout:每个
await调用都包一层asyncio.wait_for(..., timeout=2.0),防止单个慢请求拖垮整批特征
真正难的不是写 async 关键字,而是理清哪些 IO 是真异步(HTTP、Redis)、哪些只是假异步(用线程池包同步库)、以及下游系统能不能扛住你并发出来的流量。没做压测就上 gather,和没加锁一样危险。


















