如何确定对sys.stdin.readline()的调用(或者更一般地说,对任何基于文件描述符的文件对象的readline()调用)是否将被阻止?
当我用python编写一个基于行的文本过滤程序时,就会出现这种情况;
我想实现一个合理的输出缓冲策略。
-
当处理数百万个
-
当保持缓冲输出时,它不应该阻塞输入。
因此,无缓冲输出是不好的,因为它违反了(1)(对操作系统的写入太多)。
(在一百万行中的每一行批量刷新输出到操作系统是没有意义的)。
默认的缓冲输出是不好的,因为它违反了(2)(如果输出是到文件或管道,它将不适当地保留输出)。
“每当(其缓冲区已满或)sys.stdin.readline()即将阻塞时,刷新sys.stdout。”。
这能实现吗?
(请注意,我并不认为这种策略是完美的
案例。例如,
更频繁地刷新,以避免在执行长时间计算时保留输出。)
(实际上,“cat-n”比一次一行更聪明;也就是说,它知道怎么做
在整行被读之前读写一行的一部分;
但是,在这个例子中,我还是要一行一行地实现它。)
行缓冲实现
(表现良好,但违反标准(1),即由于冲洗过多,速度慢得不合理):
#!/usr/bin/python
# cat-n.linebuffered.py
import sys
num_lines_read = 0
while True:
line = sys.stdin.readline()
if line == '': break
num_lines_read += 1
print("%d: %s" % (num_lines_read, line))
sys.stdout.flush()
默认缓冲实现
(快速但违反标准(2),即不友好的输出保留)
#!/usr/bin/python
# cat-n.defaultbuffered.py
import sys
num_lines_read = 0
while True:
line = sys.stdin.readline()
if line == '': break
num_lines_read += 1
print("%d: %s" % (num_lines_read, line))
预期实施:
#!/usr/bin/python
num_lines_read = 0
while True:
if sys_stdin_readline_is_about_to_block(): # <--- How do I implement this??
sys.stdout.flush()
line = sys.stdin.readline()
if line == '': break
num_lines_read += 1
print("%d: %s" % (num_lines_read, line))
sys_stdin_readline_is_about_to_block()
我想要一个既适用于python2又适用于python3的答案。
我已经研究了下面的每一种技术,但是到目前为止还没有什么结果。
-
select([sys.stdin],[],[],0)
以确定从sys.stdin读取是否会阻塞(当sys.stdin是缓冲文件对象时,这不起作用,原因至少有一个,也可能有两个:(1)如果部分行准备好从底层输入管道读取,它会错误地说“will not block”,(2)如果sys.stdin的缓冲区包含一个完整的输入行,但是底层管道没有准备好进行额外的读取,那么它会错误地说“will block”。。。我想是的)。
-
os.fdopen(sys.stdin.fileno(), 'r')
和
fcntl
具有
O_NONBLOCK
(我无法在任何python版本中使用readline():
在python2.7中,每当部分行进入时,它就会丢失输入;
在python3中,似乎无法区分“will block”
-
asyncio
-
创建一个线程来执行
readline()
循环并将每条线传递到主管道
程序通过一个队列。队列;然后主程序可以在
从中读取每一行,当它看到它要阻塞时,首先刷新stdout。
(我试过这个,实际上它工作了,见下文,但它非常慢,比行缓冲慢得多。)
线程化实现:
请注意,这并没有严格地回答“如何判断sys.stdin.readline()是否要阻塞”的问题,但它还是设法实现了所需的缓冲策略。不过太慢了。
#!/usr/bin/python
# cat-n.threaded.py
import queue
import sys
import threading
def iter_with_abouttoblock_cb(callable, sentinel, abouttoblock_cb, qsize=100):
# child will send each item through q to parent.
q = queue.Queue(qsize)
def child_fun():
for item in iter(callable, sentinel):
q.put(item)
q.put(sentinel)
child = threading.Thread(target=child_fun)
# The child thread normally runs until it sees the sentinel,
# but we mark it daemon so that it won't prevent the parent
# from exiting prematurely if it wants.
child.daemon = True
child.start()
while True:
try:
item = q.get(block=False)
except queue.Empty:
# q is empty; call abouttoblock_cb before blocking
abouttoblock_cb()
item = q.get(block=True)
if item == sentinel:
break # do *not* yield sentinel
yield item
child.join()
num_lines_read = 0
for line in iter_with_abouttoblock_cb(sys.stdin.readline,
sentinel='',
abouttoblock_cb=sys.stdout.flush):
num_lines_read += 1
sys.stdout.write("%d: %s" % (num_lines_read, line))
下面的命令(在linux上的bash中)显示了预期的缓冲行为:“defaultbuffered”缓冲区过于激进,而“linebuffered”和“threaded”缓冲区正好相反。
| cat
在管道的末尾,默认情况下是使python块缓冲区而不是行缓冲区。)
for which in defaultbuffered linebuffered threaded; do
for python in python2.7 python3.5; do
echo "$python cat-n.$which.py:"
(echo z; echo -n a; sleep 1; echo b; sleep 1; echo -n c; sleep 1; echo d; echo x; echo y; echo z; sleep 1; echo -n e; sleep 1; echo f) | $python cat-n.$which.py | cat
done
done
输出:
python2.7 cat-n.defaultbuffered.py:
[... pauses 5 seconds here. Bad! ...]
1: z
2: ab
3: cd
4: x
5: y
6: z
7: ef
python3.5 cat-n.defaultbuffered.py:
[same]
python2.7 cat-n.linebuffered.py:
1: z
[... pauses 1 second here, as expected ...]
2: ab
[... pauses 2 seconds here, as expected ...]
3: cd
4: x
5: y
6: z
[... pauses 2 seconds here, as expected ...]
6: ef
python3.5 cat-n.linebuffered.py:
[same]
python2.7 cat-n.threaded.py:
[same]
python3.5 cat-n.threaded.py:
[same]
时间安排:
(在linux上的bash中):
for which in defaultbuffered linebuffered threaded; do
for python in python2.7 python3.5; do
echo -n "$python cat-n.$which.py: "
timings=$(time (yes 01234567890123456789012345678901234567890123456789012345678901234567890123456789 | head -1000000 | $python cat-n.$which.py >| /tmp/REMOVE_ME) 2>&1)
echo $timings
done
done
/bin/rm /tmp/REMOVE_ME
输出:
python2.7 cat-n.defaultbuffered.py: real 0m1.490s user 0m1.191s sys 0m0.386s
python3.5 cat-n.defaultbuffered.py: real 0m1.633s user 0m1.007s sys 0m0.311s
python2.7 cat-n.linebuffered.py: real 0m5.248s user 0m2.198s sys 0m2.704s
python3.5 cat-n.linebuffered.py: real 0m6.462s user 0m3.038s sys 0m3.224s
python2.7 cat-n.threaded.py: real 0m25.097s user 0m18.392s sys 0m16.483s
python3.5 cat-n.threaded.py: real 0m12.655s user 0m11.722s sys 0m1.540s
重申一下,我想要一个在保持缓冲输出时从不阻塞的解决方案
(在这方面,“linebuffered”和“threaded”都是好的),
而且速度也很快:也就是说,在速度上与“defaultbuffered”相当。