
本文详解使用 asyncio 读取长期运行子进程 stdout 的关键要点,重点解决因输出缓冲导致的 readline() 返回空字节、communicate() 失效等问题,并提供可立即复用的完整示例。
本文详解使用 asyncio 读取长期运行子进程 stdout 的关键要点,重点解决因输出缓冲导致的 `readline()` 返回空字节、`communicate()` 失效等问题,并提供可立即复用的完整示例。
在 asyncio 中通过 create_subprocess_exec 启动子进程并读取其 stdout 是常见需求,但实践中常因输出缓冲(buffering) 导致 readline() 持续返回空字节(b'')或阻塞——这并非 asyncio 使用错误,而是子进程 Python 脚本默认行缓冲/全缓冲行为所致。
以文中“秒表”程序为例:其每秒 print(start),但若未显式刷新或禁用缓冲,输出会暂存于内存缓冲区,直到缓冲满、换行或进程退出才真正写入管道。而子进程 stdout=PIPE 后,父进程 readline() 依赖的是实际到达管道的数据流,缓冲未刷出即无数据可读。
✅ 正确解法是强制子进程标准输出非缓冲化:
在启动命令中添加 -u 参数(Python 的 unbuffered 模式),确保 print() 立即写入:
self.cmd = ["python", "-u", "stopwatch.py", "10"]
同时,stop() 方法中需循环读取直至 EOF(而非仅调用一次 readline()),因为 kill() 后子进程可能仍有残留输出未被消费;await proc.wait() 前必须清空管道,否则可能引发资源泄漏或死锁:
async def stop(self):
if self.robot:
print("======Stopping robot======")
self.robot.kill()
# 循环读取所有可用 stdout 行,直到 EOF(readline() 返回 b'')
while True:
line = await self.robot.stdout.readline()
if not line: # EOF reached
break
print(f"[stdout] {line.decode().strip()}")
await self.robot.wait() # 等待进程彻底终止
print("======Robot terminated======")
self.robot = None
⚠️ 注意事项:
- ❌ 避免在 start() 中直接 await proc.stdout.readline() —— 若子进程尚未输出第一行,将永久阻塞;
- ❌ 不要对已 kill() 的进程重复调用 communicate(),它会等待进程结束,但 kill() 后需手动消费 stdout;
- ✅ 若子进程非 Python 编写,可尝试 stdbuf -oL(Linux)或重定向 1>/dev/stdout 等方式强制行缓冲;
- ✅ 生产环境建议增加超时控制(如 asyncio.wait_for(..., timeout=5))防止管道阻塞导致协程挂起。
完整可运行示例整合如下:
import asyncio
class RobotManager:
def __init__(self):
self.cmd = ["python", "-u", "stopwatch.py", "10"] # 关键:-u 禁用缓冲
self.robot = None
async def start(self):
print("======Starting robot======")
self.robot = await asyncio.create_subprocess_exec(
*self.cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT, # 合并 stderr 到 stdout 方便统一处理
)
async def stop(self):
if self.robot:
print("======Stopping robot======")
self.robot.kill()
# 持续读取 stdout 直到流关闭
while True:
line = await self.robot.stdout.readline()
if line == b'': # EOF
break
print(f"[stopwatch] {line.decode().rstrip()}")
await self.robot.wait()
print("======Robot terminated======")
self.robot = None
async def main():
robot = RobotManager()
await robot.start()
await asyncio.sleep(3)
await robot.stop()
await robot.start()
await asyncio.sleep(3)
await robot.stop()
if __name__ == "__main__":
asyncio.run(main())
总结:asyncio 读取子进程 stdout 的核心在于 “子进程输出实时性” + “父进程消费完整性”。启用 -u 是 Python 子进程的黄金实践;循环 readline() + wait() 是安全终止的标配模式。掌握这两点,即可稳健实现异步进程通信。










