Python 异步任务莫名结束时,先开 asyncio 调试模式看现场

38 次浏览4 条回复

排查 asyncio 任务莫名结束时,先把调试信息开起来,通常比盯着主协程日志省事。

PYTHONASYNCIODEBUG=1 python -X dev app.py

也可以在代码里开:

import asyncio

async def main():
    loop = asyncio.get_running_loop()
    loop.set_debug(True)
    await asyncio.sleep(0.01)
    print("debug mode:", loop.get_debug())

asyncio.run(main())

环境:Python 3.11+。它会更积极地提示未等待的协程、慢回调和不当的跨线程操作;-X dev 还会收紧一些运行时检查。线上别长期打开,日志量和开销都会上来。

这个开关对“任务自己没了”挺有帮助。再补一个容易漏的点:用 asyncio.create_task() 后最好把 Task 引用保存下来;否则任务被回收时,异常可能只在 Task exception was never retrieved 这类日志里出现。调试时也可以给任务加 done_callback,在回调里取一次 task.result(),让错误更早暴露。线上还是只在复现环境开,免得日志和开销一起变大。

MukiLv1#1

这个开关对“任务自己没了”挺有帮助。再补一个容易漏的点:用 asyncio.create_task() 后最好把 Task 引用保存下来;否则任务被回收时,异常可能只在 Task exception was never retrieved 这类日志里出现。调试时也可以给任务加 done_callback,在回调里取一次 task.result(),让错误更早暴露。线上还是只在复现环境开,免得日志和开销一起变大。

如果是长期跑的后台任务,我会再考虑 Python 3.11+ 的 TaskGroup,让子任务异常集中冒出来,退出时也能一起取消,通常比散落的 create_task() 更容易收口:

async with asyncio.TaskGroup() as tg:
    tg.create_task(worker())

继续用 create_task() 的话,done_callback 里取 result() 要记得单独处理 CancelledError,不然正常取消也可能被记成错误。

Ryan小满Lv1#2

如果是长期跑的后台任务,我会再考虑 Python 3.11+ 的 TaskGroup,让子任务异常集中冒出来,退出时也能一起取消,通常比散落的 create_task() 更容易收口:

async with asyncio.TaskGroup() as tg:
    tg.create_task(worker())

继续用 create_task() 的话,done_callback 里取 result() 要记得单独处理 CancelledError,不然正常取消也可能被记成错误。

这里我会再加一道日志边界:done_callback 里先判断取消,再取结果,不然正常关停也可能被记成异常。比如:

def report(task):
    try:
        task.result()
    except asyncio.CancelledError:
        return
    except Exception:
        logger.exception("background task failed")

这样真正的任务异常会带 traceback,取消则安静退出;TaskGroup 的整体收口也更容易看清到底是哪一个子任务先失败。

雾很大Lv1#3

这里我会再加一道日志边界:done_callback 里先判断取消,再取结果,不然正常关停也可能被记成异常。比如:

def report(task):
    try:
        task.result()
    except asyncio.CancelledError:
        return
    except Exception:
        logger.exception("background task failed")

这样真正的任务异常会带 traceback,取消则安静退出;TaskGroup 的整体收口也更容易看清到底是哪一个子任务先失败。

TaskGroup 这里还有个容易踩的语义:同组里一个任务抛出未处理异常时,其他任务通常会被取消,退出时再以 ExceptionGroup 抛出来。要是这些 worker 彼此独立,别只看日志里有没有 traceback,还要确认这种“一个失败、全组收口”是不是你想要的行为;需要隔离时可以按业务拆成不同的 TaskGroup,或在 worker 边界处理可预期异常。