Python 为什么`popen.stdout.readline`会出现死锁?该怎么办?

Python 为什么`popen.stdout.readline`会出现死锁?该怎么办?,python,python-3.x,subprocess,Python,Python 3.x,Subprocess,从 警告使用communicate()而不是.stdin.write、.stdout.read或.stderr.read,以避免由于任何其他操作系统管道缓冲区填满并阻塞子进程而导致死锁 我在试着理解为什么会出现僵局。在某些背景下,我并行生成了N个进程: for c in commands: h = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

警告使用
communicate()
而不是
.stdin.write
.stdout.read
.stderr.read
,以避免由于任何其他操作系统管道缓冲区填满并阻塞子进程而导致死锁

我在试着理解为什么会出现僵局。在某些背景下,我并行生成了N个进程:

for c in commands:
    h = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
    handles.append(h)
然后逐个打印每个过程的输出:

for handle in handles:
    while handle.poll() is None:
        try:
            line = handle.stdout.readline()
        except UnicodeDecodeError:
            line = "((INVALID UNICODE))\n"

        sys.stdout.write(line)
    if handle.returncode != 0:
        print(handle.stdout.read(), file=sys.stdout)
    if handle.returncode != 0:
        print(handle.stderr.read(), file=sys.stderr)
事实上,有时这会造成僵局。不幸的是,文档中建议使用
communicate()
对我来说是行不通的,因为这个过程可能需要几分钟才能运行,我不希望它在这段时间内看起来死气沉沉。它应该实时打印输出

我有几个选项,比如更改
bufsize
参数,为每个句柄在不同的线程中轮询,等等。但是为了决定解决这个问题的最佳方法,我想我首先需要了解死锁的根本原因是什么。很明显,这与缓冲区大小有关,但是什么呢?我可以假设,可能所有这些进程都共享一个操作系统内核对象,因为我只会耗尽其中一个进程的缓冲区,其他进程会填满它,在这种情况下,上面的选项2可能会修复它。但也许这甚至不是真正的问题


有人能解释一下吗?

父进程和子进程之间的双向通信使用两个单向管道。每个方向一个。好的,stderr是第三个,但是想法是一样的

管子有两端,一个用来写字,一个用来读书。管道的容量是4K,在现代Linux上现在是64K。在其他系统上可以预期类似的值。这意味着,写入程序可以在不超过其限制的情况下写入管道,但随后管道将满,对管道的写入将阻塞,直到读取器从另一端读取一些数据

从读者的角度来看,情况是显而易见的。在数据可用之前,常规读取会一直阻塞

总而言之:当进程试图从无人写入的管道中读取数据时,或者当它将大于管道容量的数据写入无人读取的管道时,就会发生死锁

通常,这两个进程充当客户机和服务器,并利用某种请求/响应方式的通信。有点像半双工。一边在写,另一边在读。然后他们转换角色。这实际上是我们可以用标准同步编程处理的最复杂的设置。当客户端和服务器不同步时,死锁仍然会发生。这可能是由空响应、意外错误消息等引起的

如果有几个子进程,或者当通信协议不是那么简单,或者我们只是想要一个健壮的解决方案时,我们需要父进程对所有管道进行操作<代码>通信()为此目的使用线程。另一种方法是异步I/O:首先检查哪种方法准备好进行I/O,然后才从该管道(或套接字)读取或写入。旧的和不推荐使用的库实现了这一点

在低级别上,select(或类似)系统调用检查给定集合中的哪些文件句柄已准备好进行I/O。但在该低级别上,在重新检查之前,我们只能执行一次读取或写入操作。这就是这个片段的问题:

while handle.poll() is None:
    try:
        line = handle.stdout.readline()
    except UnicodeDecodeError:
        line = "((INVALID UNICODE))\n"
poll
检查告诉我们有东西要读,但这并不意味着我们可以重复阅读,直到换行!我们只能执行一次读取并将数据附加到输入缓冲区。如果有换行符,我们可以提取整行并处理它。如果没有,我们需要等待下一次成功的轮询并读取

写的行为也类似。我们可以写一次,检查写入的字节数,并从输出缓冲区中删除这么多字节

这意味着需要在此基础上实现行缓冲和所有更高级别的东西。幸运的是,
asyncore
的后续版本提供了我们所需要的:

我希望我能解释一下僵局。解决方案是可以预料的。如果需要执行多项操作,请使用线程或
asyncio


更新:

下面是一个简短的异步IO测试程序。它从几个子进程读取输入,并逐行打印数据

但是首先是一个
cmd.py
helper,它将一行打印成几个小块,以演示行缓冲。请尝试使用,例如使用
python3 cmd.py 10

import sys
import time

def countdown(n):
    print('START', n)
    while n >= 0: 
        print(n, end=' ', flush=True)
        time.sleep(0.1)
        n -= 1
    print('END')

if __name__ == '__main__':
    args = sys.argv[1:]
    if len(args) != 1:
        sys.exit(3)
    countdown(int(args[0]))
主要节目是:

import asyncio

PROG = 'cmd.py'
NPROC = 12

async def run1(*execv):
    """Run a program, read input lines."""
    proc = await asyncio.create_subprocess_exec(
        *execv,
        stdin=asyncio.subprocess.DEVNULL,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.DEVNULL)
    # proc.stdout is a StreamReader object
    async for line in proc.stdout:
        print("Got line:", line.decode().strip())

async def manager(prog, nproc):
    """Spawn 'nproc' copies of python script 'prog'."""
    tasks = [asyncio.create_task(run1('python3', prog, str(i))) for i in range(nproc)]
    await asyncio.wait(tasks)

if __name__ == '__main__':
    asyncio.run(manager(PROG, NPROC))
async for line…
StreamReader
的一项功能,类似于
for line in file:
习惯用法。它可以替换为:

    while True:
        line = await proc.stdout.readline()
        if not line:
            break
        print("Got line:", line.decode().strip())

read
readline
的问题是它们正在阻止调用。如果您使用
读线
,并且用
\n
回复过程需要时间,则该过程将被阻止,直到那时<代码>读取(1)可以代替
readline
。但是,如果启动的进程没有输出,
handle.poll()
应该足以保证对
read(1)
的调用不会被阻止,那么这也会阻止进程。但是你所说的与文件不完全相符。我上面引用的文档表明它与操作系统管道缓冲区有关,而不是缺少换行符。如果您看到
call
函数,它会说
不要在该函数中使用stdout=pipe或stderr=pipe。如果子进程生成足够的输出到管道,以填充操作系统管道缓冲区,则子进程将阻塞,因为没有从中读取管道。
。这是因为他们说你不应该通读这些管道,你不应该使用它们。现在回到
readline
thing,readline意味着读取数据直到换行。所以如果你读一个需要时间的程序