asyncio.run() 每次新建事件循环是设计使然,内部调用 new_event_loop() 并关闭,不可复用;嵌入异步上下文时应改用 await 或 create_task。

asyncio.run() 为什么每次都会新建事件循环
因为 asyncio.run() 的设计目标就是“开箱即用、用完即弃”——它内部会调用 asyncio.new_event_loop(),设置为当前线程的默认循环,执行完就调用 loop.close()。这不是 bug,是刻意为之的隔离策略。
常见错误现象:RuntimeError: asyncio.run() cannot be called from a running event loop,本质是你在已有循环里(比如 Jupyter、Django async view、或已手动 asyncio.get_event_loop().run_until_complete() 后)又调了 asyncio.run()。
- 如果你在脚本顶层调用,放心用;但嵌入到已有异步上下文时,直接换
await coro或loop.create_task() -
asyncio.run()不接受loop参数,无法复用旧循环——这是 API 层面的硬限制 - 重复调用
asyncio.run()会触发多次循环创建/关闭,有开销;高频短任务建议自己管理循环生命周期
loop.run_until_complete() 和 loop.run_forever() 的分工边界
loop.run_until_complete() 是“执行一个协程直到结束”,返回其结果;loop.run_forever() 是“让循环一直转着,等外部调用 loop.stop() 才退出”。两者底层都调用 CPython 的 PyEval_RestoreThread() 和 epoll/kqueue 轮询,但语义完全不同。
使用场景:写 CLI 工具常用前者;写长驻服务(如 WebSocket 服务器主循环)必须用后者,否则协程结束整个程序就退出了。
立即学习“Python免费学习笔记(深入)”;
诚客在线考试是由南宁诚客网络科技有限公司开发的一款手机移动端的答题网站软件,它应用广泛适合各种学校、培训班、教育机构、公司企业、事业单位、各种社会团体、银行证券等用于学生学习刷题、员工内部培训,学员考核、员工对公司制度政策的学习……可使用的题型有:单选题、多选题、判断题支持文字,图片,音频,视频、数学公式。可以设置考试时间,答题时间,考试次数,是否需要补考,是否可以看到自己成绩。练习模式,支持学生
-
run_until_complete()内部会自动处理循环未启动、已关闭等状态,但要求传入的是coroutine对象,不是普通函数 -
run_forever()启动后,循环进入阻塞等待状态,你得靠loop.call_soon()、loop.create_task()或信号回调来驱动逻辑 - 别在
run_forever()后写代码——它不返回,除非你手动loop.stop(),否则下面的语句永远不会执行
asyncio.get_event_loop() 在不同 Python 版本下的行为差异
Python 3.7+ 默认启用 “当前线程单例循环” 策略,asyncio.get_event_loop() 会返回当前线程绑定的循环(若无则新建);而 3.6 及更早版本,在非主线程里首次调用会抛 RuntimeError,必须显式 set_event_loop()。
这个差异导致多线程 + asyncio 混用时极易出错,尤其在测试或 Web 框架后台任务中。
- 3.7+ 中,子线程默认没有循环,第一次调用
get_event_loop()会新建并绑定到该线程——但这个循环不会自动运行,需手动run_until_complete() - 跨线程传递协程对象是危险的:协程对象绑定创建它的循环,不能在另一个循环里
await - 安全做法:每个线程用自己的
asyncio.new_event_loop(),并用loop.set_task_factory()避免意外复用
事件循环如何调度 task 和 callback:从 _ready 到 _scheduled
循环内部维护两个核心队列:_ready(就绪队列,存 ready 的 callback 和 task)和 _scheduled(定时队列,按时间戳排序)。每次循环迭代先清空 _ready,再从 _scheduled 拿到期任务进 _ready。
这就是为什么 asyncio.sleep(0) 能让出控制权:它把当前 task 推进 _scheduled 并设为“立刻到期”,下一轮迭代才重新入 _ready,从而给其他 task 插入机会。
- task 对象本身不直接进队列,而是被包装成
callbacks存入_ready;所以create_task()立刻可被调度,但实际执行时机取决于当前轮次是否还有剩余 callback -
call_soon()进_ready头部,call_later()进_scheduled,call_at()也是_scheduled但指定绝对时间 - 别依赖
_ready的顺序做逻辑——它受 callback 注册顺序、C 扩展调用、甚至 GC 触发时机影响,只保证 FIFO 大致成立
_scheduled 里有没有漏掉的 timeout,或者 _ready 是否被某个没加 await 的协程对象悄悄撑爆。







