【问题标题】:Does select() behave differently on python2 and python3?select() 在 python2 和 python3 上的行为是否不同?
【发布时间】:2015-01-16 09:12:47
【问题描述】:

我想从this 帖子中描述的同一线程中的子进程中读取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,如发布的链接中所述:

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 总是在有可用数据时返回,但在这种情况下,每 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);
}}

【问题讨论】:

  • 您是否尝试过显而易见的事情:显式调用.flush()-u 在 Python 3 中的行为不同——流是行缓冲的);设置bufsize=0;使用os.read(fd, 512) 而不是.readline()?代码本身已损坏,例如:即使p.poll() is not None 也可能有数据缓冲
  • 另一个变化是 sys.stdout/err 在 3.x 中变成了 io.TextIOWrapper 对象,具有跨平台的一致行为(和默认值)。以前,对于 CPython,它们是文件对象,对使用的特定 C 编译器的 stdio 结构进行了薄薄的包装。我建议仔细阅读 io 模块文档。
  • @J.F.Sebastian:子进程不必是 Python 程序。执行相同操作(并显式刷新)的 C++ 程序将显示相同的效果。 .poll() 也没有影响 - 您可以用 while True 替换该行。目前我非常确信select() 在 Python3 中被破坏了。
  • @frans:细节很重要。如果你按照我的建议使用 os.read(fd, 512) 会发生什么?
  • @J.F.Sebastian: 你对bufsize=0 的提示是对的(还有read())我没有再次检查它,因为我已经这样做了,但我想我还有另一个问题那时候。不幸的是,我不能将此标记为答案,但您的 cmets 值得投票。

标签: linux select python-3.x pipe python-2.x


【解决方案1】:

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

之后,管道中没有未读数据,因此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)。请参阅此说明:

BufferedIOBase 方法的签名与 RawIOBase 的签名大多相同(例外: write() 返回 None , read() 的参数是可选的),但可能有不同的语义。特别是,BufferedIOBase 实现可能会读取比请求更多的数据,或者使用缓冲区延迟写入数据。

【讨论】:

  • 看来你是对的,将 bufsize 设置为 0(正如 J.F. Sebastian 也建议的那样)就可以了。但我仍然不明白为什么 Python 2 的行为会有所不同。 Python2 read() 不是也被缓冲了吗?
  • @frans,似乎在 Python2 中, readline() 只读取一行并返回它,而在 Python3 中它读取所有可用数据并返回第一行。我已经更新了答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-20
  • 1970-01-01
相关资源
最近更新 更多