select() 在 python2 和 python3 上的行为是否不同?

Does select() behave differently on python2 and python3?

我想从 this post 中描述的同一线程中的子进程中读取 stdoutstderr。虽然 运行 Python2.7 中的代码按预期工作,但 Python3.3 中的 select() 调用似乎没有达到应有的效果。

看看 - 这是一个脚本,它会在 stdoutstderr 上打印两行,然后等待并重复几次:

import time, sys
for i in range(5):
    sys.stdout.write("std: %d\n" % i)
    sys.stdout.write("std: %d\n" % i)
    sys.stderr.write("err: %d\n" % i)
    sys.stderr.write("err: %d\n" % i)
    time.sleep(2)

有问题的脚本将在子进程中启动上面的脚本并读取它的 stdoutstderr,如 posted link:

import subprocess
import select

p = subprocess.Popen(['/usr/bin/env', 'python', '-u', 'test-output.py'],
                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)

r = [p.stdout.fileno(), p.stderr.fileno()]

while p.poll() is None:
    print("select")
    ret = select.select(r, [], [])

    for fd in ret[0]:
        if fd == p.stdout.fileno():
            print("readline std")
            print("stdout: " + p.stdout.readline().decode().strip())
        if fd == p.stderr.fileno():
            print("readline err")
            print("stderr: " + p.stderr.readline().decode().strip())

请注意,我使用 -u 选项启动 Python 子进程,这导致 Python 不缓冲 stdoutstderr。此外,我在调用 select()readline() 之前打印了一些文本以查看脚本阻塞的位置。

这就是问题所在: 运行 Python3 中的脚本,在每个循环之后输出阻塞 2 秒,尽管事实上,两个更多行正在等待阅读。正如每次调用 select() 之前的文本所示,您可以看到阻塞的是 select()(而不是 readline())。

我的第一个想法是 select() 仅在 Python3 刷新时恢复,而 Python2 它 returns 总是在有可用数据时恢复,但在这种情况下只有一行将每 2 秒读取一次(事实并非如此!)

所以我的问题是: 这是 Python3-select() 中的错误吗?我是否误解了 select() 的行为?有没有一种方法可以解决此问题而不必为每个管道启动一个线程?

当运行Python3时的输出:

select
readline std
stdout: std: 0
readline err
stderr: err: 0
select            <--- here the script blocks for 2 seconds
readline std
stdout: std: 0
select
readline std
stdout: std: 1
readline err
stderr: err: 0
select            <--- here the script should block (but doesn't)
readline err
stderr: err: 1
select            <--- here the script blocks for 2 seconds
readline std
stdout: std: 1
readline err
stderr: err: 1
select            <--- here the script should block (but doesn't)
readline std
stdout: std: 2
readline err
stderr: err: 2
select
.
.

编辑: 请注意,子进程是否为Python脚本没有影响。以下 C++ 程序具有相同的效果:

int main() {
    for (int i = 0; i < 4; ++i) {
        std::cout << "out: " << i << std::endl;
        std::cout << "out: " << i << std::endl;
        std::cerr << "err: " << i << std::endl;
        std::cerr << "err: " << i << std::endl;
        fflush(stdout);
        fflush(stderr);
        usleep(2000000);
}}

似乎原因是在 subprocess.PIPE 中缓冲,第一个 readline() 调用读取所有可用数据(即两行)和 returns 第一个。

之后管道中没有未读数据,所以select()不会立即返回。您可以通过将 readline 调用加倍来检查这一点:

print("stdout: " + p.stdout.readline().decode().strip())
print("stdout: " + p.stdout.readline().decode().strip())

并确保第二个 readline() 调用不会阻塞。

一种解决方案是使用 bufsize=0:

禁用缓冲
p = subprocess.Popen(['/usr/bin/env', 'python', '-u', 'test-output.py'],
                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)

另一种可能的解决方案是进行非阻塞 readline() 或询问管道文件对象其读取缓冲区大小,但我不知道是否可行。

也可以直接从p.stdout.fileno()读取实现非阻塞readline().

更新: Python2 对比 Python3

这里Python3不同于Python2的原因很可能是在新的I/O模块中(PEP 31136)。请参阅此注释:

The BufferedIOBase methods signatures are mostly identical to that of RawIOBase (exceptions: write() returns None , read() 's argument is optional), but may have different semantics. In particular, BufferedIOBase implementations may read more data than requested or delay writing data using buffers.