asyncio疑问:取消任务为何导致事件循环中其他任务被取消?
我用runTimes协程多次将slowPrinter协程加入事件循环,但中途取消runTimes任务后,不仅不再添加新任务,就连已加入事件循环的任务似乎也被取消。我在Spyder环境运行脚本,无需调用run_until_complete等方法,因为事件循环已在后台运行。
我确认代码按预期执行,但无法理解原因,同时想知道如何在取消runTimes的前提下,让slowPrinter任务仍能完成。
原始代码
import asyncio loop = asyncio.get_event_loop() async def runTimes(async_func, times): for i in range(0, times): task = loop.create_task(async_func()) await task async def slowPrinter(): await asyncio.sleep(2) print("slowPrinter done") async def doStuff(): for i in range(0, 10): await(asyncio.sleep(1)) print("doStuff done") async def doLater(delay_ms, method, *args, **kwargs): try: print("doLater " + str(delay_ms) + " " + str(method.__name__)) except AttributeError: print("doLater " + str(delay_ms)) await asyncio.sleep(delay_ms/1000) method(*args, **kwargs) print("doLater complete") task = loop.create_task(runTimes(slowPrinter, 3)) loop.create_task(doLater(3000, task.cancel)) loop.create_task(doStuff())
实际输出
doLater 3000 cancel slowPrinter done doLater complete doStuff done
预期输出
doLater 3000 cancel slowPrinter done doLater complete slowPrinter done doStuff done
编辑说明
我未使用run_later等方法,因后续要将代码移植到MicroPython,仅使用兼容函数。
编辑补充
任务取消似乎会传播到协程内部创建的任务!
修改后的runTimes代码:
async def runTimes(async_func, times): for i in range(0, times): task = loop.create_task(async_func()) try: await task except asyncio.CancelledError: print("cancelled as well")
对应输出:
doLater 3000 cancel slowPrinter done doLater complete cancelled as well slowPrinter done doStuff done
核心原因
你看到的“已添加任务被取消”是误解,实际时间线如下:
- 程序启动后,
runTimes立即创建第一个slowPrinter任务并等待它完成(耗时2秒)。 - 3秒后
doLater触发runTimes任务的取消操作,此时第一个slowPrinter已经执行完毕。 - 取消信号会在
runTimes的await语句处抛出CancelledError,如果没捕获这个异常,runTimes会直接终止,根本没机会进入下一次循环创建第二个slowPrinter任务。
你补充的代码中捕获了CancelledError,所以runTimes在触发取消后没有直接终止,而是继续执行循环,创建了第二个slowPrinter,因此输出里多了对应的slowPrinter done。
另外要明确:用loop.create_task创建的任务是独立于父任务的,父任务被取消不会自动取消子任务,子任务会继续在事件循环中执行。
解决方案
要实现“取消runTimes后,已创建的slowPrinter任务完成,且未创建的任务也能被执行”(或者至少已启动的任务不受影响),可以按以下方式处理:
方案1:捕获CancelledError,让runTimes完成剩余循环
修改runTimes,在循环外层捕获取消异常,确保即使自身被取消,已创建的任务继续执行,且能完成剩余的任务创建:
async def runTimes(async_func, times): try: for i in range(times): task = loop.create_task(async_func()) await task except asyncio.CancelledError: print("runTimes 被取消,但已创建的任务会继续执行") # 如果需要,可以在这里处理未完成的任务逻辑
方案2:不等待子任务,直接批量创建所有任务
如果你不需要等待每个slowPrinter完成后再创建下一个,而是一次性把所有任务都加入事件循环,那么即使runTimes被取消,所有slowPrinter任务都已经存在于事件循环中,会自行执行完毕:
async def runTimes(async_func, times): tasks = [] for i in range(times): task = loop.create_task(async_func()) tasks.append(task) await asyncio.gather(*tasks)
这种情况下,即使runTimes被取消,已经创建的tasks仍然会在事件循环中执行,直到完成。
关键结论
loop.create_task创建的任务是独立的,父任务的取消不会传递给子任务。- 你之前的问题本质是
runTimes被取消后,没有机会创建后续任务,而非已创建任务被取消。 - 捕获
CancelledError是让runTimes继续执行或优雅退出的关键。
内容的提问来源于stack exchange,提问作者Vitalize0769

