Python asyncio.run_forever()卡死或阻塞的常见原因及解决方法

一、问题现象描述

在使用asyncio.get_event_loop().run_forever()方法时,开发者经常遇到事件循环意外阻塞卡死的情况。典型表现包括:

  • 程序无响应且CPU占用率异常升高
  • 键盘中断(Ctrl+C)无法终止进程
  • 日志输出突然停止但进程仍在运行

二、根本原因分析

2.1 协程泄漏(Coroutine Leak)

最常见的卡死原因是未完成的协程堆积。当使用create_task()创建任务但未妥善处理时:

async def faulty_task():
    while True:  # 无限循环且无await
        pass

loop = asyncio.get_event_loop()
loop.create_task(faulty_task())  # 泄漏的协程
loop.run_forever()

这类CPU密集型协程会独占事件循环,因为缺少await语句导致无法切换上下文。

2.2 同步阻塞调用

在协程中混用同步I/O操作会直接阻塞事件循环:

async def blocking_call():
    time.sleep(10)  # 同步睡眠
    requests.get(url)  # 同步HTTP请求

这类调用会导致整个事件循环停止响应,违反异步编程的基本原则。

2.3 异常处理缺失

未捕获的异常会使任务静默失败

async def crash_task():
    raise RuntimeError("unhandled")

loop.create_task(crash_task())  # 异常未被捕获
loop.run_forever()

虽然任务已终止,但事件循环仍会持续运行。

三、解决方案

3.1 使用健康检查机制

实现看门狗定时器监控事件循环状态:

async def watchdog():
    while True:
        await asyncio.sleep(5)
        if not loop.is_running():
            loop.stop()

3.2 正确管理任务生命周期

通过Task对象跟踪所有任务:

tasks = set()

async def safe_task():
    try:
        # 业务逻辑
    finally:
        tasks.discard(asyncio.current_task())

task = loop.create_task(safe_task())
tasks.add(task)

3.3 替换危险API调用

将同步操作替换为异步等效实现:

同步API异步替代方案
time.sleep()asyncio.sleep()
requests.get()aiohttp.ClientSession.get()
open()aiofiles.open()

四、高级调试技巧

4.1 使用asyncio调试模式

启用慢回调检测

loop.set_debug(True)
loop.slow_callback_duration = 0.1  # 100ms阈值

4.2 分析事件循环状态

通过asyncio.all_tasks()检查存活任务:

def dump_tasks():
    for t in asyncio.all_tasks(loop):
        print(f"{t.get_name()} {t.done()}")