线程读取子进程stderr时的死锁问题:处理KeyboardInterrupt与SystemExit
解决Popen上下文管理器与stderr读取线程的挂起问题
问题根源
你的代码挂起的核心原因是:
- 子进程
cat在无输入时会持续运行,读取线程调用proc.stderr.read(1)后会一直阻塞,等待stderr输出。 - Popen上下文管理器的
__exit__方法会尝试关闭stderr文件描述符,但此时读取线程仍在对该fd执行阻塞读取操作,导致close()调用被阻塞,无法完成上下文退出。即使触发KeyboardInterrupt,也会卡在stderr.close()步骤。
解决方案
要覆盖正常退出、KeyboardInterrupt、SystemExit等所有场景,需要确保:子进程被及时终止,读取线程能快速退出,避免阻塞stderr.close()操作。以下是具体实现:
1. 改进读取线程逻辑,避免无限阻塞
使用select给读取操作添加超时,同时定期检查子进程状态,子进程结束后立即终止读取循环。
2. 用try/finally确保子进程被终止
无论是否触发异常,都在退出上下文前终止子进程,让读取线程的阻塞操作因子进程结束而返回。
完整代码示例
import time import select from subprocess import Popen, PIPE from threading import Thread def read_from_stderr(proc): # 子进程存活时才尝试读取 while proc.poll() is None: # 用select设置0.5秒超时,避免无限阻塞在read上 ready_fds, _, _ = select.select([proc.stderr], [], [], 0.5) if ready_fds: try: chunk = proc.stderr.read(1) # 这里可以添加stderr内容的处理逻辑 if not chunk: # 读取到EOF,子进程已关闭stderr break except Exception: pass # 可选:读取子进程结束后剩余的stderr内容 remaining_stderr = proc.stderr.read() if remaining_stderr: # 处理剩余内容... pass with Popen(['cat'], stderr=PIPE) as proc: print("Starting thread to read stderr") read_thread = Thread(target=read_from_stderr, args=(proc,)) read_thread.start() try: # 这里放置你的自定义业务逻辑 time.sleep(1) finally: # 确保无论是否触发异常,都终止子进程 proc.terminate() # 等待读取线程完全退出,释放资源 read_thread.join(timeout=2) # 加超时防止线程意外挂起 print("Exiting the context. No hang occurs.")
方案说明
select.select:给读取操作添加超时,避免线程在子进程无输出时一直阻塞,同时定期检查子进程状态。try/finally:保证proc.terminate()一定会被执行,无论是否触发KeyboardInterrupt或其他异常,子进程会被终止,读取线程的循环会因proc.poll() is not None而退出。read_thread.join():等待读取线程完全结束后再退出上下文,确保stderr文件描述符没有被占用,上下文管理器的close()操作可以顺利完成。
内容的提问来源于stack exchange,提问作者Michal Charemza
相关产品推荐
相关产品推荐

