为何使用run_forever启动的asyncio事件循环在无未完成任务时无法提交新任务?
这个问题我之前调试asyncio多线程模型时也踩过坑,核心原因在于asyncio事件循环的休眠唤醒机制,再加上你代码里的一个线程安全问题,咱们一步步拆解:
1. 事件循环run_forever()的本质
当你调用loop.run_forever()时,事件循环会持续运行,但如果当前没有任何待处理的任务、I/O事件或回调,它会进入阻塞休眠状态,等待外部触发唤醒(比如新的I/O事件、定时器到期,或者手动调用唤醒接口)。
你第一次的场景里,启动循环后没有任何任务,循环直接进入休眠。此时你调用loop.create_task(dostuff(0)),虽然任务被加入了循环的任务队列,但这个操作不会主动唤醒休眠的事件循环——循环还在“睡大觉”,根本不知道有新任务进来,自然不会去执行它。
2. 为什么加了“存活任务”就正常了?
你写的main()任务是一个持续循环的协程,每隔0.1秒调用await asyncio.sleep(0.1)。这个sleep本质是向事件循环注册了一个0.1秒后的定时器回调:
- 每次sleep到期,事件循环会被唤醒,处理这个回调,然后回到协程继续执行循环
- 在每次唤醒的间隙,事件循环会检查任务队列里的新任务,所以你后续提交的
dostuff(0)就能被发现并执行
相当于这个存活任务一直在“踢”事件循环,让它没法进入深度休眠,自然能及时处理新任务。
3. 正确的解决方案(不用依赖存活任务)
首先要纠正一个线程安全问题:不要在非事件循环线程直接调用loop.create_task(),这个方法不是线程安全的,可能导致任务队列的竞态问题。正确的做法是用asyncio.run_coroutine_threadsafe()或者loop.call_soon_threadsafe()来提交任务,这两个方法会自动唤醒事件循环:
方案一:用asyncio.run_coroutine_threadsafe提交任务
import asyncio, threading, time loop = asyncio.new_event_loop() thread = threading.Thread(target=loop.run_forever) thread.start() async def dostuff(i): await asyncio.sleep(5) print("doing something", i) # 用线程安全的方式提交任务,自动唤醒循环 future = asyncio.run_coroutine_threadsafe(dostuff(0), loop) # 如果需要获取结果可以用future.result()
方案二:手动唤醒事件循环
如果你坚持用create_task(不推荐,因为非线程安全),提交任务后需要手动调用唤醒接口:
loop.create_task(dostuff(0)) # 手动唤醒事件循环,让它检查新任务 loop.call_soon_threadsafe(lambda: None)
方案三:启动循环前先添加一个空的“唤醒触发器”
可以在启动循环前提交一个一次性的回调,确保循环不会直接进入休眠:
loop.call_soon(lambda: None) thread = threading.Thread(target=loop.run_forever) thread.start()
这样循环启动后会先处理这个空回调,之后虽然可能进入休眠,但后续用线程安全的方式提交任务时,会自动唤醒循环。
总结
asyncio事件循环在无任务时会休眠,新任务不会自动唤醒它;加上跨线程提交任务的线程安全问题,就导致了你看到的现象。用线程安全的任务提交方法,就能让新任务被及时处理,不需要依赖一个持续运行的“存活任务”。
内容的提问来源于stack exchange,提问作者matt

