asyncio.run()中logging.info()卡住是因为logging默认同步io会阻塞事件循环;应使用queuehandler+queuelistener将日志io移至后台线程,避免协程内直接创建filehandler等同步处理器。

为什么asyncio.run()里用logging.info()会卡住?
因为 logging 默认使用同步文件写入,底层调用的是阻塞式 open() 和 write()。在 asyncio.run() 启动的事件循环中,一旦某个协程里触发日志(尤其是首次初始化 handler 时),logging 会尝试获取锁、刷磁盘、甚至加载配置——这些全都会让当前协程挂起,而事件循环又无法切走(因为没 await),结果整个程序“看起来卡死”。常见现象是:打印第一条日志后,后续 await asyncio.sleep(1) 不执行,控制台无响应。
- 首次调用
logging.info()触发FileHandler初始化,会同步打开文件 - 日志量大时,
StreamHandler写sys.stderr也可能因缓冲区满而阻塞 -
RotatingFileHandler在轮转时会重命名/删除旧文件,是典型 IO 密集操作
用QueueHandler + QueueListener解耦日志IO
这是 Python 官方推荐的异步安全方案:把日志记录请求发到线程安全队列,由独立线程消费并执行实际 IO。关键不在“异步写”,而在“不阻塞事件循环”。
- 主协程只做
logger.info()→ 入队(纯内存操作,毫秒级) - 另起一个后台线程运行
QueueListener,它调用handler.emit(),此时 IO 发生在线程中,不影响 asyncio - 必须禁用原有 handler(如
root.handlers = []),否则日志会被同步 handler 截获 -
QueueListener的respect_handler_level=True要设为True,否则低级别日志(如 DEBUG)可能被忽略
import logging
import logging.handlers
from logging.handlers import QueueHandler, QueueListener
import queue
import asyncio
<p>log_queue = queue.Queue()
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)</p><h1>清空默认 handler,避免同步写</h1><p>for h in logger.handlers[:]:
logger.removeHandler(h)</p><h1>绑定队列 handler</h1><p>queue_handler = QueueHandler(log_queue)
logger.addHandler(queue_handler)</p><h1>后台线程处理实际 IO</h1><p>file_handler = logging.FileHandler("app.log")
console_handler = logging.StreamHandler()
listener = QueueListener(log_queue, file_handler, console_handler, respect_handler_level=True)
listener.start()</p><h1>此时在协程里调用完全安全</h1><p>async def main():
logger.info("start") # 不阻塞
await asyncio.sleep(0.1)
logger.error("done") # 也不阻塞
listener.stop() # 退出前关闭监听器</p>
避免在协程中直接创建FileHandler或RotatingFileHandler
哪怕只是实例化 RotatingFileHandler("x.log"),也会触发文件存在性检查、大小读取、甚至首次轮转判断——这些全是同步系统调用。在 asyncio 环境下,这等价于在协程里调 os.stat()。
- 不要在
async def函数体内 new 任何继承自FileHandler的 handler - 不要在
__aenter__或setup_logging()协程里初始化 handler;应在主线程、事件循环启动前完成 - 如果必须动态生成日志路径(比如按用户 ID 分日志),用
os.path.join()拼接字符串可以,但别调os.makedirs(..., exist_ok=True)—— 改用loop.run_in_executor(None, os.makedirs, path) -
TimedRotatingFileHandler更危险:它内部有定时器线程,且首次构造时就检查文件修改时间
简单场景下,用StringIO + flush 到 async 写入
如果只是临时调试、日志量极小、且不需要滚动或持久化,可以用内存 buffer 替代文件 IO。但注意:这不是生产方案,仅用于验证逻辑是否被阻塞。
- 用
io.StringIO作为 handler 的 stream,所有emit()只写内存 - 定期(如每 10 条或每秒)用
await aiofiles.open(...).write()异步刷出,避免堆积 - 必须自己加锁(
asyncio.Lock)防止多协程并发写同一 buffer - 别用
print()替代——它底层仍是sys.stdout.write(),在 Windows 或某些终端下会锁 stdout
真正麻烦的不是“怎么记日志”,而是“谁来负责 IO”和“何时释放 GIL”。QueueListener 方案里那个后台线程,才是绕过 asyncio 限制的关键支点。漏掉 listener.start() 或忘记 listener.stop(),都可能导致日志丢失或进程无法退出。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











