为sys.stdin设置较小的缓冲区大小?

27

我正在使用以下Bash命令模式运行memcached:

memcached -vv 2>&1 | tee memkeywatch2010098.log 2>&1 | ~/bin/memtracer.py | tee memkeywatchCounts20100908.log

尝试追踪平台范围内键的未匹配的get和set。

下面是memtracer脚本,它按预期工作,只有一个小问题。观察中间日志文件的大小,memtracer.py直到memkeywatchYMD.log大小约为15-18K时才开始获取输入。是否有更好的方法来读取标准输入,或者可以将缓冲区大小缩小到不到1k以提高响应时间?

#!/usr/bin/python

import sys
from collections import defaultdict

if __name__ == "__main__":


    keys = defaultdict(int)
    GET = 1
    SET = 2
    CLIENT = 1
    SERVER = 2

    #if <
    for line in sys.stdin:
        key = None
        components = line.strip().split(" ")
        #newConn = components[0][1:3]
        direction = CLIENT if components[0].startswith("<") else SERVER

        #if lastConn != newConn:        
        #    lastConn = newConn

        if direction == CLIENT:            
            command = SET if components[1] == "set" else GET
            key = components[2]
            if command == SET:                
                keys[key] -= 1                                                                                    
        elif direction == SERVER:
            command = components[1]
            if command == "sending":
                key = components[3] 
                keys[key] += 1

        if key != None:
            print "%s:%s" % ( key, keys[key], )
6个回答

38

您可以通过使用Python的-u标志,完全从stdin/stdout中删除缓冲:

-u     : unbuffered binary stdout and stderr (also PYTHONUNBUFFERED=x)
         see man page for details on internal buffering relating to '-u'

而man页阐明了:

   -u     Force stdin, stdout and stderr to  be  totally  unbuffered.   On
          systems  where  it matters, also put stdin, stdout and stderr in
          binary mode.  Note that there is internal  buffering  in  xread-
          lines(),  readlines()  and  file-object  iterators ("for line in
          sys.stdin") which is not influenced by  this  option.   To  work
          around  this, you will want to use "sys.stdin.readline()" inside
          a "while 1:" loop.

除此之外,更改现有文件的缓冲区不受支持,但是您可以使用os.fdopen使用与现有文件相同的基本文件描述符创建一个新文件对象,并可能具有不同的缓冲区。

import os
import sys
newin = os.fdopen(sys.stdin.fileno(), 'r', 100)

应该newin 绑定到一个文件对象的名称上,该文件对象读取与标准输入相同的FD,但每次只缓冲大约100字节(您可以使用sys.stdin = newin继续使用新的文件对象作为标准输入)。 我说“应该”,因为这个领域过去在某些平台上存在许多错误和问题(要提供具有完全通用性的跨平台功能非常困难) - 我不确定它现在的状态如何,但我肯定建议在所有感兴趣的平台上进行彻底的测试,以确保一切顺利。(-u完全移除缓冲,在所有平台上都能较少出现问题,如果符合您的要求,可以考虑使用)。


14
不幸的是,Python 3 仍然顽固地以缓冲文本模式打开 stdin。现在,只有 stdoutstderr-u 选项影响。 - Martijn Pieters
有没有Python3的解决方案?也许是事件驱动的库/选项? - Brad Hein
我尝试使用gio_channels,并使其工作-但行为完全相同:直到按下“enter”才有输出。 - jcoppens
3
这段代码在Python 3.4.3中对我有效:os.fdopen(sys.stdin.fileno(), 'rb', buffering=0) - Denilson Sá Maia
2
@DenilsonSáMaia:不需要自己重新打开它。sys.stdin实际上有三层;一个io.TextIOWrapper(将bytes解码为str)包装了一个io.BufferedReader(缓冲bytes)包装了一个io.FileIO(提交系统调用的实际事物)。它们都可以作为属性使用;sys.stdin.buffer获取未进行文本解码的BufferedReadersys.stding.buffer.raw获取未缓冲的FileIO - ShadowRanger
显示剩余3条评论

23
你可以直接使用 sys.stdin.readline() 方法代替 sys.stdin.__iter__() 方法:
import sys

while True:
    line = sys.stdin.readline()
    if not line: break # EOF

    sys.stdout.write('> ' + line.upper())

这使我在使用Python 2.7.4和Python 3.3.1在Ubuntu 13.04上进行行缓冲读取。


2
这与问题无关,您是想将其作为评论吗? - David
2
据我所理解,问题是“有没有更好的方法来读取stdin”[避免在管道中使用Python脚本时出现输入缓冲区问题],我的答案(虽然晚了三年)是:“是的,请使用readline而不是__iter__”。但也许我的答案依赖于平台,如果您尝试上述代码,仍然会出现缓冲区问题? - Søren Løvborg
Ahkey,我明白了。我的意思是stdin缓冲区的大小要小得多(例如80字节或更少)。对于2.7版本,如果没有Alex在他的回答中提到的-U标志,您无法影响这些缓冲区大小。 - David
2
有趣的是Alex没有注意到这一点,https://github.com/certik/python-2.7/blob/c360290c3c9e55fbd79d6ceacdfc7cd4f393c1eb/Objects/fileobject.c#L1377 你说得对,readline很可能更快,因为它使用逐步获取getc,而file_internext则缓冲8192,如源代码中所定义。 - David
这非常重要--我看到由于缓冲stdin(而不是立即反应),程序不够交互。我以前不知道这一点。 - dan3

12

使用iter的双参数形式创建一个sys.stdin.readline的迭代器,由于sys.stdin.__iter__仍然是行缓冲的,因此可以获得一个行为几乎相同的迭代器(在EOF处停止,而stdin.__iter__不会停止):

import sys

for line in iter(sys.stdin.readline, ''):
    sys.stdout.write('> ' + line.upper())

或者将None作为哨兵(但请注意,这样您需要自己处理EOF条件)。


2
这似乎更适合作为对Soren答案的评论。Alex Martelli和Soren已经提供了答案,而这更像是对Soren输入的改进。 - David
1
你在这里提出的解决方案是我见过的最好的解决办法,我将扫描所有我的Python代码并用它替换“for line in sys.stdin”。我看到它实际上已经列在了你提到的参考页面中。但仍然不清楚的是,“for line in sys.stdin”为什么与“for line in iter(sys.stdin.readline, ''):”的行为不同呢?就我所知,它们在语义上是相同的,除了前者版本的行为看起来像一个讨厌的bug,没有人会想要这种行为。如果有任何反例,我很乐意看到它。 - Don Hatch
@DonHatch 当在stdin上进行迭代时,我同意行为很奇怪并且类似于bug,但是当文件不是stdin时,一次读取8k将会提高性能。 - Sam Jacobson
@SamJacobson 为什么输入流是标准输入还是其他流会有所影响呢?(也许您想指出终端、文件和管道之间的某些差异?但这些差异与是否为标准输入无关。)当您说一次读取8k可以提高性能时,与什么相比可以提高性能?我认为我从未提出或倡导过任何行为,当输入中有8k可用时,不会少于一次读取8k。 - Don Hatch
@SamJacobson 顺便说一下,我之前已经在 https://bugs.python.org/issue26290 上报告了这个问题:“fileinput 和 'for line in sys.stdin' 对输入缓冲做出了奇怪的嘲弄”。 - Don Hatch
这绝对是Python 2上的正确解决方案;在Python 2中,for line in sys.stdin:使用内部用户模式缓冲,直到填充一个块才产生任何行,并且除了确保不使用file.__next__-u无效)之外,没有办法禁用它;唯一的解决方案是像这里演示的那样使用file.readline,或者重新包装使用io模块以获得Python 3的行为(即使启用缓冲,也不会阻塞直到填充一个块;它本质上是一个系统调用,如果短读包括换行符,则可以进行短读)。 - ShadowRanger

6

这对我在Python 3.4.3中有效:

import os
import sys

unbuffered_stdin = os.fdopen(sys.stdin.fileno(), 'rb', buffering=0)

fdopen()的文档称其只是open()的别名。

open()有一个可选的buffering参数:

buffering是一个可选的整数,用于设置缓冲策略。传递0以关闭缓冲(仅允许在二进制模式下),传递1以选择行缓冲(仅在文本模式下可用),传递大于1的整数表示固定大小块缓冲区的字节数。

换句话说:

  • 完全不带缓冲的stdin需要二进制模式并将缓冲区大小设置为零。
  • 行缓冲需要文本模式。
  • 任何其他缓冲区大小似乎都可以在二进制文本模式下使用(根据文档)。

3

也许你的问题不是出在Python上,而是由于Linux shell在使用管道连接命令时注入的缓冲区所致。当这种情况发生时,输入并不是按行缓冲,而是按4K块缓冲。

要解决这个问题,可以在命令链前加上expect包中的unbuffer命令,例如:

unbuffer memcached -vv 2>&1 | unbuffer -p tee memkeywatch2010098.log 2>&1 | unbuffer -p ~/bin/memtracer.py | tee memkeywatchCounts20100908.log
< p >在管道中使用unbuffer命令时需要添加-p选项。


0

我在Python 2.7中唯一能做到的方法是:

tty.setcbreak(sys.stdin.fileno())

来自Python非阻塞控制台输入。这将完全禁用缓冲并抑制回显。

编辑:关于Alex的答案,第一个建议(使用-u调用python)在我的情况下不可行(请参见shebang限制)。

第二个建议(使用较小缓冲区复制fd:os.fdopen(sys.stdin.fileno(),'r',100))在我使用0或1的缓冲区时无法工作,因为它是交互式输入,我需要立即处理每个按键。


奇怪,Alex的答案当时对我起作用了。不知道是否有一个回溯更新改变/破坏了什么。 - David
"tty.setcbreak" 不涉及 Python 缓冲,而是关于内核 tty 层缓冲输入。因此,它不适用于管道。 - textshell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接