使用 Popen.wait() 时重现死锁

rok*_*rok 8 python deadlock subprocess popen python-3.x

从使用Popen.wait()的文档可以:

当使用 stdout=PIPE 和/或 stderr=PIPE 并且子进程向管道生成足够的输出时死锁,从而阻止等待 OS 管道缓冲区接受更多数据。使用communication() 来避免这种情况。

在交流文档中,它写道:

读取的数据是缓存在内存中的,所以如果数据量很大或者没有限制就不要使用这种方式

如何重现这种有问题的行为并看到使用Popen.communicate()修复它?

死锁意味着持有资源的进程之间会发生一些循环等待并且永远卡住。这里的循环依赖是什么?等待子进程终止的 Python 进程是一种等待。另一个是什么?在下面的场景中,谁在等待什么?

它阻止等待 OS 管道缓冲区接受更多数据

Jea*_*bre 8

这很简单。

创建一个输出大量文本而不读取输出的进程:

p = subprocess.Popen(["ls","-R"],stdout=subprocess.PIPE)
p.wait()
Run Code Online (Sandbox Code Playgroud)

一段时间后,标准输出管道已满,进程被阻塞。

这是一个死锁情况,因为子进程不能再写入输出,直到它被消耗(即:从不),并且 python 进程等待子进程完成。

为避免死锁,您可以使用读取行循环:

p = subprocess.Popen(["ls","-R"],stdout=subprocess.PIPE)
for line in p.stdout:
    # do something with the line
p.wait()
Run Code Online (Sandbox Code Playgroud)

communicate还修复了这个问题,但也修复了更棘手的情况,即输出错误流都被重定向到单独的流(在这种情况下,上面的幼稚循环仍然可能死锁)。

假设你有一个编译过程

p = subprocess.Popen(["gcc","-c"]+mega_list_of_files,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
Run Code Online (Sandbox Code Playgroud)

现在你想得到这个输出,所以你做:

output = p.stdout.read()
Run Code Online (Sandbox Code Playgroud)

不幸的是,反而弹出了很多错误,在您读取输出流时阻塞了错误流:再次死锁。

尝试改为读取错误流,可能会发生完全相反的情况:大量 stdout 输出阻塞了您的进程。

communicate使用多线程能够同时处理输出和错误流并将它们分开,没有阻塞的风险。唯一需要注意的是,您无法实时逐行控制进程输出/打印程序输出:

p = subprocess.Popen(["gcc","-c"]+mega_list_of_files,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
output,error = p.communicate()
return_code = p.wait()
Run Code Online (Sandbox Code Playgroud)

  • 是的,你的 python 进程。当然,如果输出是无限的,则通信永无止境并消耗所有内存。在这种情况下,例如,您必须在遇到给定行时终止该进程。在这种情况下,您必须逐行阅读。 (2认同)